diff --git a/.changeset/calm-pandas-stop.md b/.changeset/calm-pandas-stop.md new file mode 100644 index 0000000000..b13fc8cecf --- /dev/null +++ b/.changeset/calm-pandas-stop.md @@ -0,0 +1,6 @@ +--- +'e2b': patch +'@e2b/python-sdk': patch +--- + +Add an opt-in `scope` option for killing a command's process group while preserving process-only termination by default. diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 3bb6277943..b5e00c5651 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -524,7 +524,7 @@ export interface paths { }; cookie?: never; }; - requestBody: { + requestBody?: { content: { "application/json": components["schemas"]["ResumedSandbox"]; }; @@ -2231,23 +2231,6 @@ export interface components { * @description CPU cores for the sandbox */ CPUCount: number; - CreatedAccessToken: { - /** - * Format: date-time - * @description Timestamp of access token creation - */ - createdAt: string; - /** - * Format: uuid - * @description Identifier of the access token - */ - id: string; - mask: components["schemas"]["IdentifierMaskingDetails"]; - /** @description Name of the access token */ - name: string; - /** @description The fully created access token */ - token: string; - }; CreatedTeamAPIKey: { /** * Format: date-time @@ -2427,10 +2410,6 @@ export interface components { * @description Memory for the sandbox in MiB */ MemoryMB: number; - NewAccessToken: { - /** @description Name of the access token */ - name: string; - }; NewSandbox: { /** @description Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. */ allow_internet_access?: boolean; @@ -2641,6 +2620,73 @@ export interface components { */ timeout?: number; }; + /** @description An orchestrator node pool backed by one cloud scaling group */ + Rig: { + /** + * Format: int32 + * @description Number of instances currently attached to the rig + */ + capacityCurrent: number; + /** + * Format: int32 + * @description Desired number of instances in the rig + */ + capacityDesired: number; + /** + * Format: int32 + * @description Maximum capacity enforced on the rig's scaling group. Omitted when nothing enforces bounds (GCP MIG without an active autoscaler). + */ + capacityMax?: number; + /** + * Format: int32 + * @description Minimum capacity enforced on the rig's scaling group. Omitted when nothing enforces bounds (GCP MIG without an active autoscaler). + */ + capacityMin?: number; + /** @description Rig identifier (e.g. "default") */ + id: string; + /** @description Cloud provider backing the rig ("aws" or "gcp") */ + provider: string; + /** @description Canonical cloud resource ID of the scaling group backing the rig (ARN on AWS, self-link on GCP) */ + resourceID: string; + }; + /** @description Desired capacity to set on the rig's scaling group */ + RigCapacityChange: { + /** + * Format: int32 + * @description Absolute desired number of instances in the rig + */ + desired: number; + }; + /** @description Scaling error on the rig's scaling group, e.g. a failed instance creation due to resource exhaustion */ + RigError: { + /** @description Action being performed when the error occurred (e.g. CREATING) */ + action?: string; + /** @description Provider-specific error code (e.g. ZONE_RESOURCE_POOL_EXHAUSTED, Failed) */ + code: string; + /** @description Instance the error relates to, if any */ + instance?: string; + /** @description Human-readable error message */ + message: string; + /** + * Format: date-time + * @description When the error occurred + */ + timestamp: string; + }; + /** @description An instance attached to a rig's scaling group */ + RigInstance: { + /** + * Format: date-time + * @description When the provider created the instance. Omitted while the instance is transitioning. + */ + createdAt?: string; + /** @description Provider instance ID (EC2 instance ID on AWS, instance name on GCP), also the node ID the orchestrator reports */ + id: string; + /** @description The instance is on its way out of the group and can never become healthy again */ + terminating: boolean; + /** @description The provider is creating, deleting, recreating or otherwise mutating the instance */ + transitioning: boolean; + }; Sandbox: { /** @description Alias of the template */ alias?: string; @@ -2860,9 +2906,11 @@ export interface components { /** @description List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. */ denyOut?: string[]; egressProxy?: components["schemas"]["SandboxEgressProxyConfig"]; + /** @description Sandbox ports that serve HTTPS rather than plaintext HTTP. Affects how the proxy reaches the service inside the sandbox; the public URL is HTTPS either way. Certificates are not verified, so self-signed ones work. The envd port (49983) cannot be listed. */ + httpsPorts?: number[]; /** @description Specify host mask which will be used for all sandbox requests */ maskRequestHost?: string; - /** @description Per-domain transform rules applied to matching egress HTTP/HTTPS requests. Keys are domains (e.g. "api.example.com", "example.com"). A domain listed here is not automatically allowed - use allowOut to permit the traffic. */ + /** @description Per-domain transform rules applied to matching outbound HTTPS requests. Keys may be exact DNS names (for example, "api.example.com") or a leading wildcard (for example, "*.example.com"), and are normalized to lowercase on write. Wildcards match subdomains at any depth but not the apex domain; a bare "*" is invalid. Exact rules take precedence, followed by the longest matching wildcard suffix, and matching rule sets are not merged. Broad wildcards such as "*.com" are allowed and may expose transformed credentials to every matching destination the sandbox contacts. Rules do not grant network access; configure allowOut separately to permit the destination. */ rules?: { [key: string]: components["schemas"]["SandboxNetworkRule"][]; }; @@ -2887,7 +2935,7 @@ export interface components { /** @description List of denied CIDR blocks or IP addresses for egress traffic. Domain names are not supported for deny rules. */ denyOut?: string[]; egressProxy?: components["schemas"]["SandboxEgressProxyConfig"]; - /** @description Per-domain transform rules. Replaces all existing rules when provided. */ + /** @description Per-domain transform rules applied to matching outbound HTTPS requests. Replaces all existing rules when provided. Keys may be exact DNS names or a single leading wildcard (for example, "*.example.com"), and are normalized to lowercase on write. Wildcards match subdomains at any depth but not the apex domain; a bare "*" is invalid. Exact rules take precedence, followed by the longest matching wildcard suffix, and matching rule sets are not merged. Broad wildcards such as "*.com" are allowed and may expose transformed credentials to every matching destination the sandbox contacts. Rules do not grant network access; configure allowOut separately to permit the destination. */ rules?: { [key: string]: components["schemas"]["SandboxNetworkRule"][]; }; @@ -3421,8 +3469,8 @@ export interface components { "application/json": components["schemas"]["Error"]; }; }; - /** @description Gone */ - 410: { + /** @description Too many requests */ + 429: { headers: { [name: string]: unknown; }; @@ -3430,8 +3478,8 @@ export interface components { "application/json": components["schemas"]["Error"]; }; }; - /** @description Too many requests */ - 429: { + /** @description Server error */ + 500: { headers: { [name: string]: unknown; }; @@ -3439,8 +3487,8 @@ export interface components { "application/json": components["schemas"]["Error"]; }; }; - /** @description Server error */ - 500: { + /** @description Not implemented by this deployment */ + 501: { headers: { [name: string]: unknown; }; @@ -3477,14 +3525,17 @@ export interface components { }; }; parameters: { - accessTokenID: string; apiKeyID: string; buildID: string; + /** @description Identifier of the cluster */ + clusterID: string; nodeID: string; /** @description Maximum number of items to return per page */ paginationLimit: number; /** @description Cursor to start the list from */ paginationNextToken: string; + /** @description Rig identifier (e.g. "default") */ + rigID: string; sandboxID: string; secretID: string; snapshotID: string; diff --git a/packages/js-sdk/src/envd/process/process_pb.ts b/packages/js-sdk/src/envd/process/process_pb.ts index 9376b83653..f188efcede 100644 --- a/packages/js-sdk/src/envd/process/process_pb.ts +++ b/packages/js-sdk/src/envd/process/process_pb.ts @@ -22,7 +22,7 @@ import type { Message } from '@bufbuild/protobuf' export const file_process_process: GenFile = /*@__PURE__*/ fileDesc( - 'ChVwcm9jZXNzL3Byb2Nlc3MucHJvdG8SB3Byb2Nlc3MiSgoDUFRZEh8KBHNpemUYASABKAsyES5wcm9jZXNzLlBUWS5TaXplGiIKBFNpemUSDAoEY29scxgBIAEoDRIMCgRyb3dzGAIgASgNIqEBCg1Qcm9jZXNzQ29uZmlnEgsKA2NtZBgBIAEoCRIMCgRhcmdzGAIgAygJEi4KBGVudnMYAyADKAsyIC5wcm9jZXNzLlByb2Nlc3NDb25maWcuRW52c0VudHJ5EhAKA2N3ZBgEIAEoCUgAiAEBGisKCUVudnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgYKBF9jd2QiDQoLTGlzdFJlcXVlc3QiXAoLUHJvY2Vzc0luZm8SJgoGY29uZmlnGAEgASgLMhYucHJvY2Vzcy5Qcm9jZXNzQ29uZmlnEgsKA3BpZBgCIAEoDRIQCgN0YWcYAyABKAlIAIgBAUIGCgRfdGFnIjcKDExpc3RSZXNwb25zZRInCglwcm9jZXNzZXMYASADKAsyFC5wcm9jZXNzLlByb2Nlc3NJbmZvIpcBCgxTdGFydFJlcXVlc3QSJwoHcHJvY2VzcxgBIAEoCzIWLnByb2Nlc3MuUHJvY2Vzc0NvbmZpZxIeCgNwdHkYAiABKAsyDC5wcm9jZXNzLlBUWUgAiAEBEhAKA3RhZxgDIAEoCUgBiAEBEhIKBXN0ZGluGAQgASgISAKIAQFCBgoEX3B0eUIGCgRfdGFnQggKBl9zdGRpbiJiCg1VcGRhdGVSZXF1ZXN0EikKB3Byb2Nlc3MYASABKAsyGC5wcm9jZXNzLlByb2Nlc3NTZWxlY3RvchIeCgNwdHkYAiABKAsyDC5wcm9jZXNzLlBUWUgAiAEBQgYKBF9wdHkiEAoOVXBkYXRlUmVzcG9uc2UirwMKDFByb2Nlc3NFdmVudBIxCgVzdGFydBgBIAEoCzIgLnByb2Nlc3MuUHJvY2Vzc0V2ZW50LlN0YXJ0RXZlbnRIABIvCgRkYXRhGAIgASgLMh8ucHJvY2Vzcy5Qcm9jZXNzRXZlbnQuRGF0YUV2ZW50SAASLQoDZW5kGAMgASgLMh4ucHJvY2Vzcy5Qcm9jZXNzRXZlbnQuRW5kRXZlbnRIABI0CglrZWVwYWxpdmUYBCABKAsyHy5wcm9jZXNzLlByb2Nlc3NFdmVudC5LZWVwQWxpdmVIABoZCgpTdGFydEV2ZW50EgsKA3BpZBgBIAEoDRpICglEYXRhRXZlbnQSEAoGc3Rkb3V0GAEgASgMSAASEAoGc3RkZXJyGAIgASgMSAASDQoDcHR5GAMgASgMSABCCAoGb3V0cHV0GlsKCEVuZEV2ZW50EhEKCWV4aXRfY29kZRgBIAEoERIOCgZleGl0ZWQYAiABKAgSDgoGc3RhdHVzGAMgASgJEhIKBWVycm9yGAQgASgJSACIAQFCCAoGX2Vycm9yGgsKCUtlZXBBbGl2ZUIHCgVldmVudCI1Cg1TdGFydFJlc3BvbnNlEiQKBWV2ZW50GAEgASgLMhUucHJvY2Vzcy5Qcm9jZXNzRXZlbnQiNwoPQ29ubmVjdFJlc3BvbnNlEiQKBWV2ZW50GAEgASgLMhUucHJvY2Vzcy5Qcm9jZXNzRXZlbnQiYwoQU2VuZElucHV0UmVxdWVzdBIpCgdwcm9jZXNzGAEgASgLMhgucHJvY2Vzcy5Qcm9jZXNzU2VsZWN0b3ISJAoFaW5wdXQYAiABKAsyFS5wcm9jZXNzLlByb2Nlc3NJbnB1dCITChFTZW5kSW5wdXRSZXNwb25zZSI3CgxQcm9jZXNzSW5wdXQSDwoFc3RkaW4YASABKAxIABINCgNwdHkYAiABKAxIAEIHCgVpbnB1dCLCAgoSU3RyZWFtSW5wdXRSZXF1ZXN0EjcKBXN0YXJ0GAEgASgLMiYucHJvY2Vzcy5TdHJlYW1JbnB1dFJlcXVlc3QuU3RhcnRFdmVudEgAEjUKBGRhdGEYAiABKAsyJS5wcm9jZXNzLlN0cmVhbUlucHV0UmVxdWVzdC5EYXRhRXZlbnRIABI6CglrZWVwYWxpdmUYAyABKAsyJS5wcm9jZXNzLlN0cmVhbUlucHV0UmVxdWVzdC5LZWVwQWxpdmVIABo3CgpTdGFydEV2ZW50EikKB3Byb2Nlc3MYASABKAsyGC5wcm9jZXNzLlByb2Nlc3NTZWxlY3RvchoxCglEYXRhRXZlbnQSJAoFaW5wdXQYAiABKAsyFS5wcm9jZXNzLlByb2Nlc3NJbnB1dBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiFQoTU3RyZWFtSW5wdXRSZXNwb25zZSJfChFTZW5kU2lnbmFsUmVxdWVzdBIpCgdwcm9jZXNzGAEgASgLMhgucHJvY2Vzcy5Qcm9jZXNzU2VsZWN0b3ISHwoGc2lnbmFsGAIgASgOMg8ucHJvY2Vzcy5TaWduYWwiFAoSU2VuZFNpZ25hbFJlc3BvbnNlIj4KEUNsb3NlU3RkaW5SZXF1ZXN0EikKB3Byb2Nlc3MYASABKAsyGC5wcm9jZXNzLlByb2Nlc3NTZWxlY3RvciIUChJDbG9zZVN0ZGluUmVzcG9uc2UiOwoOQ29ubmVjdFJlcXVlc3QSKQoHcHJvY2VzcxgBIAEoCzIYLnByb2Nlc3MuUHJvY2Vzc1NlbGVjdG9yIjsKD1Byb2Nlc3NTZWxlY3RvchINCgNwaWQYASABKA1IABINCgN0YWcYAiABKAlIAEIKCghzZWxlY3RvcipICgZTaWduYWwSFgoSU0lHTkFMX1VOU1BFQ0lGSUVEEAASEgoOU0lHTkFMX1NJR1RFUk0QDxISCg5TSUdOQUxfU0lHS0lMTBAJMpEECgdQcm9jZXNzEjMKBExpc3QSFC5wcm9jZXNzLkxpc3RSZXF1ZXN0GhUucHJvY2Vzcy5MaXN0UmVzcG9uc2USPgoHQ29ubmVjdBIXLnByb2Nlc3MuQ29ubmVjdFJlcXVlc3QaGC5wcm9jZXNzLkNvbm5lY3RSZXNwb25zZTABEjgKBVN0YXJ0EhUucHJvY2Vzcy5TdGFydFJlcXVlc3QaFi5wcm9jZXNzLlN0YXJ0UmVzcG9uc2UwARI5CgZVcGRhdGUSFi5wcm9jZXNzLlVwZGF0ZVJlcXVlc3QaFy5wcm9jZXNzLlVwZGF0ZVJlc3BvbnNlEkoKC1N0cmVhbUlucHV0EhsucHJvY2Vzcy5TdHJlYW1JbnB1dFJlcXVlc3QaHC5wcm9jZXNzLlN0cmVhbUlucHV0UmVzcG9uc2UoARJCCglTZW5kSW5wdXQSGS5wcm9jZXNzLlNlbmRJbnB1dFJlcXVlc3QaGi5wcm9jZXNzLlNlbmRJbnB1dFJlc3BvbnNlEkUKClNlbmRTaWduYWwSGi5wcm9jZXNzLlNlbmRTaWduYWxSZXF1ZXN0GhsucHJvY2Vzcy5TZW5kU2lnbmFsUmVzcG9uc2USRQoKQ2xvc2VTdGRpbhIaLnByb2Nlc3MuQ2xvc2VTdGRpblJlcXVlc3QaGy5wcm9jZXNzLkNsb3NlU3RkaW5SZXNwb25zZUJXCgtjb20ucHJvY2Vzc0IMUHJvY2Vzc1Byb3RvUAGiAgNQWFiqAgdQcm9jZXNzygIHUHJvY2Vzc+ICE1Byb2Nlc3NcR1BCTWV0YWRhdGHqAgdQcm9jZXNzYgZwcm90bzM' + 'ChVwcm9jZXNzL3Byb2Nlc3MucHJvdG8SB3Byb2Nlc3MiSgoDUFRZEh8KBHNpemUYASABKAsyES5wcm9jZXNzLlBUWS5TaXplGiIKBFNpemUSDAoEY29scxgBIAEoDRIMCgRyb3dzGAIgASgNIqEBCg1Qcm9jZXNzQ29uZmlnEgsKA2NtZBgBIAEoCRIMCgRhcmdzGAIgAygJEi4KBGVudnMYAyADKAsyIC5wcm9jZXNzLlByb2Nlc3NDb25maWcuRW52c0VudHJ5EhAKA2N3ZBgEIAEoCUgAiAEBGisKCUVudnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgYKBF9jd2QiDQoLTGlzdFJlcXVlc3QiXAoLUHJvY2Vzc0luZm8SJgoGY29uZmlnGAEgASgLMhYucHJvY2Vzcy5Qcm9jZXNzQ29uZmlnEgsKA3BpZBgCIAEoDRIQCgN0YWcYAyABKAlIAIgBAUIGCgRfdGFnIjcKDExpc3RSZXNwb25zZRInCglwcm9jZXNzZXMYASADKAsyFC5wcm9jZXNzLlByb2Nlc3NJbmZvIpcBCgxTdGFydFJlcXVlc3QSJwoHcHJvY2VzcxgBIAEoCzIWLnByb2Nlc3MuUHJvY2Vzc0NvbmZpZxIeCgNwdHkYAiABKAsyDC5wcm9jZXNzLlBUWUgAiAEBEhAKA3RhZxgDIAEoCUgBiAEBEhIKBXN0ZGluGAQgASgISAKIAQFCBgoEX3B0eUIGCgRfdGFnQggKBl9zdGRpbiJiCg1VcGRhdGVSZXF1ZXN0EikKB3Byb2Nlc3MYASABKAsyGC5wcm9jZXNzLlByb2Nlc3NTZWxlY3RvchIeCgNwdHkYAiABKAsyDC5wcm9jZXNzLlBUWUgAiAEBQgYKBF9wdHkiEAoOVXBkYXRlUmVzcG9uc2UirwMKDFByb2Nlc3NFdmVudBIxCgVzdGFydBgBIAEoCzIgLnByb2Nlc3MuUHJvY2Vzc0V2ZW50LlN0YXJ0RXZlbnRIABIvCgRkYXRhGAIgASgLMh8ucHJvY2Vzcy5Qcm9jZXNzRXZlbnQuRGF0YUV2ZW50SAASLQoDZW5kGAMgASgLMh4ucHJvY2Vzcy5Qcm9jZXNzRXZlbnQuRW5kRXZlbnRIABI0CglrZWVwYWxpdmUYBCABKAsyHy5wcm9jZXNzLlByb2Nlc3NFdmVudC5LZWVwQWxpdmVIABoZCgpTdGFydEV2ZW50EgsKA3BpZBgBIAEoDRpICglEYXRhRXZlbnQSEAoGc3Rkb3V0GAEgASgMSAASEAoGc3RkZXJyGAIgASgMSAASDQoDcHR5GAMgASgMSABCCAoGb3V0cHV0GlsKCEVuZEV2ZW50EhEKCWV4aXRfY29kZRgBIAEoERIOCgZleGl0ZWQYAiABKAgSDgoGc3RhdHVzGAMgASgJEhIKBWVycm9yGAQgASgJSACIAQFCCAoGX2Vycm9yGgsKCUtlZXBBbGl2ZUIHCgVldmVudCI1Cg1TdGFydFJlc3BvbnNlEiQKBWV2ZW50GAEgASgLMhUucHJvY2Vzcy5Qcm9jZXNzRXZlbnQiNwoPQ29ubmVjdFJlc3BvbnNlEiQKBWV2ZW50GAEgASgLMhUucHJvY2Vzcy5Qcm9jZXNzRXZlbnQiYwoQU2VuZElucHV0UmVxdWVzdBIpCgdwcm9jZXNzGAEgASgLMhgucHJvY2Vzcy5Qcm9jZXNzU2VsZWN0b3ISJAoFaW5wdXQYAiABKAsyFS5wcm9jZXNzLlByb2Nlc3NJbnB1dCITChFTZW5kSW5wdXRSZXNwb25zZSI3CgxQcm9jZXNzSW5wdXQSDwoFc3RkaW4YASABKAxIABINCgNwdHkYAiABKAxIAEIHCgVpbnB1dCLCAgoSU3RyZWFtSW5wdXRSZXF1ZXN0EjcKBXN0YXJ0GAEgASgLMiYucHJvY2Vzcy5TdHJlYW1JbnB1dFJlcXVlc3QuU3RhcnRFdmVudEgAEjUKBGRhdGEYAiABKAsyJS5wcm9jZXNzLlN0cmVhbUlucHV0UmVxdWVzdC5EYXRhRXZlbnRIABI6CglrZWVwYWxpdmUYAyABKAsyJS5wcm9jZXNzLlN0cmVhbUlucHV0UmVxdWVzdC5LZWVwQWxpdmVIABo3CgpTdGFydEV2ZW50EikKB3Byb2Nlc3MYASABKAsyGC5wcm9jZXNzLlByb2Nlc3NTZWxlY3RvchoxCglEYXRhRXZlbnQSJAoFaW5wdXQYAiABKAsyFS5wcm9jZXNzLlByb2Nlc3NJbnB1dBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiFQoTU3RyZWFtSW5wdXRSZXNwb25zZSJ0ChFTZW5kU2lnbmFsUmVxdWVzdBIpCgdwcm9jZXNzGAEgASgLMhgucHJvY2Vzcy5Qcm9jZXNzU2VsZWN0b3ISHwoGc2lnbmFsGAIgASgOMg8ucHJvY2Vzcy5TaWduYWwSEwoLZGVzY2VuZGFudHMYAyABKAgiFAoSU2VuZFNpZ25hbFJlc3BvbnNlIj4KEUNsb3NlU3RkaW5SZXF1ZXN0EikKB3Byb2Nlc3MYASABKAsyGC5wcm9jZXNzLlByb2Nlc3NTZWxlY3RvciIUChJDbG9zZVN0ZGluUmVzcG9uc2UiOwoOQ29ubmVjdFJlcXVlc3QSKQoHcHJvY2VzcxgBIAEoCzIYLnByb2Nlc3MuUHJvY2Vzc1NlbGVjdG9yIjsKD1Byb2Nlc3NTZWxlY3RvchINCgNwaWQYASABKA1IABINCgN0YWcYAiABKAlIAEIKCghzZWxlY3RvcipICgZTaWduYWwSFgoSU0lHTkFMX1VOU1BFQ0lGSUVEEAASEgoOU0lHTkFMX1NJR1RFUk0QDxISCg5TSUdOQUxfU0lHS0lMTBAJMpEECgdQcm9jZXNzEjMKBExpc3QSFC5wcm9jZXNzLkxpc3RSZXF1ZXN0GhUucHJvY2Vzcy5MaXN0UmVzcG9uc2USPgoHQ29ubmVjdBIXLnByb2Nlc3MuQ29ubmVjdFJlcXVlc3QaGC5wcm9jZXNzLkNvbm5lY3RSZXNwb25zZTABEjgKBVN0YXJ0EhUucHJvY2Vzcy5TdGFydFJlcXVlc3QaFi5wcm9jZXNzLlN0YXJ0UmVzcG9uc2UwARI5CgZVcGRhdGUSFi5wcm9jZXNzLlVwZGF0ZVJlcXVlc3QaFy5wcm9jZXNzLlVwZGF0ZVJlc3BvbnNlEkoKC1N0cmVhbUlucHV0EhsucHJvY2Vzcy5TdHJlYW1JbnB1dFJlcXVlc3QaHC5wcm9jZXNzLlN0cmVhbUlucHV0UmVzcG9uc2UoARJCCglTZW5kSW5wdXQSGS5wcm9jZXNzLlNlbmRJbnB1dFJlcXVlc3QaGi5wcm9jZXNzLlNlbmRJbnB1dFJlc3BvbnNlEkUKClNlbmRTaWduYWwSGi5wcm9jZXNzLlNlbmRTaWduYWxSZXF1ZXN0GhsucHJvY2Vzcy5TZW5kU2lnbmFsUmVzcG9uc2USRQoKQ2xvc2VTdGRpbhIaLnByb2Nlc3MuQ2xvc2VTdGRpblJlcXVlc3QaGy5wcm9jZXNzLkNsb3NlU3RkaW5SZXNwb25zZUJXCgtjb20ucHJvY2Vzc0IMUHJvY2Vzc1Byb3RvUAGiAgNQWFiqAgdQcm9jZXNzygIHUHJvY2Vzc+ICE1Byb2Nlc3NcR1BCTWV0YWRhdGHqAgdQcm9jZXNzYgZwcm90bzM' ) /** @@ -607,6 +607,14 @@ export type SendSignalRequest = Message<'process.SendSignalRequest'> & { * @generated from field: process.Signal signal = 2; */ signal: Signal + + /** + * Signal the command's process group instead of only its leader. + * This includes descendants that remain in the command's process group. + * + * @generated from field: bool descendants = 3; + */ + descendants: boolean } /** diff --git a/packages/js-sdk/src/envd/versions.ts b/packages/js-sdk/src/envd/versions.ts index 61434e6127..7a73540c6d 100644 --- a/packages/js-sdk/src/envd/versions.ts +++ b/packages/js-sdk/src/envd/versions.ts @@ -7,3 +7,4 @@ export const ENVD_OCTET_STREAM_UPLOAD = '0.5.7' export const ENVD_FILE_METADATA = '0.6.2' export const ENVD_VERSION_FS_EVENT_ENTRY_INFO = '0.6.3' export const ENVD_VERSION_WATCH_NETWORK_MOUNTS = '0.6.4' +export const ENVD_COMMANDS_DESCENDANTS = '0.7.1' diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index a803efcda9..2951fd354b 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -109,6 +109,8 @@ export { ALL_TRAFFIC } from './sandbox/network' export type { ProcessInfo, + CommandKillScope, + CommandKillOpts, CommandRequestOpts, CommandConnectOpts, CommandStartOpts, diff --git a/packages/js-sdk/src/sandbox/commands/commandHandle.ts b/packages/js-sdk/src/sandbox/commands/commandHandle.ts index 759d17fe51..532aa0ef1a 100644 --- a/packages/js-sdk/src/sandbox/commands/commandHandle.ts +++ b/packages/js-sdk/src/sandbox/commands/commandHandle.ts @@ -4,7 +4,7 @@ import { } from '../../envd/rpc' import { SandboxError } from '../../errors' import { ConnectResponse, StartResponse } from '../../envd/process/process_pb' -import type { CommandRequestOpts } from '.' +import type { CommandKillOpts, CommandRequestOpts } from '.' declare const __brand: unique symbol type Brand = { [__brand]: B } @@ -109,7 +109,7 @@ export class CommandHandle constructor( readonly pid: number, private readonly handleDisconnect: () => void, - private readonly handleKill: () => Promise, + private readonly handleKill: (opts?: CommandKillOpts) => Promise, private readonly events: AsyncIterable, private readonly onStdout?: (stdout: string) => void | Promise, private readonly onStderr?: (stderr: string) => void | Promise, @@ -201,10 +201,11 @@ export class CommandHandle * Kill the command. * It uses `SIGKILL` signal to kill the command. * + * @param opts kill and connection options. * @returns `true` if the command was killed successfully, `false` if the command was not found. */ - async kill() { - return await this.handleKill() + async kill(opts?: CommandKillOpts) { + return await this.handleKill(opts) } /** diff --git a/packages/js-sdk/src/sandbox/commands/index.ts b/packages/js-sdk/src/sandbox/commands/index.ts index bbc92241fb..95962fbe4a 100644 --- a/packages/js-sdk/src/sandbox/commands/index.ts +++ b/packages/js-sdk/src/sandbox/commands/index.ts @@ -29,10 +29,17 @@ import { handleRpcErrorWithHealthCheck, SandboxHealthCheck, } from '../../envd/rpc' -import { ENVD_COMMANDS_STDIN, ENVD_ENVD_CLOSE } from '../../envd/versions' +import { + ENVD_COMMANDS_DESCENDANTS, + ENVD_COMMANDS_STDIN, + ENVD_ENVD_CLOSE, +} from '../../envd/versions' import { SandboxError } from '../../errors' import { CommandHandle, CommandResult } from './commandHandle' +import { validateCommandKillScope } from './kill' +import type { CommandKillScope } from './kill' export { Pty } from './pty' +export type { CommandKillScope } from './kill' /** * Options for sending a command request. @@ -40,6 +47,18 @@ export { Pty } from './pty' export interface CommandRequestOpts extends Partial> {} +/** + * Options for killing a command. + */ +export interface CommandKillOpts extends CommandRequestOpts { + /** + * Selects whether to kill only the managed process or its process group. + * + * @default 'process' + */ + scope?: CommandKillScope +} + /** * Options for starting a new command. */ @@ -274,11 +293,21 @@ export class Commands { * It uses `SIGKILL` signal to kill the command. * * @param pid process ID of the command. You can get the list of running commands using {@link Commands.list}. - * @param opts connection options. + * @param opts kill and connection options. * * @returns `true` if the command was killed, `false` if the command was not found. */ - async kill(pid: number, opts?: CommandRequestOpts): Promise { + async kill(pid: number, opts?: CommandKillOpts): Promise { + validateCommandKillScope(opts?.scope) + if ( + opts?.scope === 'group' && + compareVersions(this.envdVersion, ENVD_COMMANDS_DESCENDANTS) < 0 + ) { + throw new SandboxError( + `Sandbox envd version ${this.envdVersion} doesn't support group-scoped command termination. Please rebuild your template to pick up the latest sandbox version.` + ) + } + try { await this.rpc.sendSignal( { @@ -289,6 +318,7 @@ export class Commands { }, }, signal: Signal.SIGKILL, + descendants: opts?.scope === 'group', }, { signal: this.connectionConfig.getSignal( @@ -356,7 +386,7 @@ export class Commands { return new CommandHandle( pid, cleanup, - () => this.kill(pid), + (killOpts) => this.kill(pid, killOpts), events, opts?.onStdout, opts?.onStderr, @@ -471,7 +501,7 @@ export class Commands { return new CommandHandle( pid, cleanup, - () => this.kill(pid), + (killOpts) => this.kill(pid, killOpts), events, opts?.onStdout, opts?.onStderr, diff --git a/packages/js-sdk/src/sandbox/commands/kill.ts b/packages/js-sdk/src/sandbox/commands/kill.ts new file mode 100644 index 0000000000..015d1a0f1a --- /dev/null +++ b/packages/js-sdk/src/sandbox/commands/kill.ts @@ -0,0 +1,19 @@ +import { InvalidArgumentError } from '../../errors' + +/** + * Scope for command termination. + * + * `process` signals only the managed command process. `group` also signals + * descendants that remain in the command's process group. + */ +export type CommandKillScope = 'process' | 'group' + +export function validateCommandKillScope( + scope: unknown +): asserts scope is CommandKillScope | undefined { + if (scope !== undefined && scope !== 'process' && scope !== 'group') { + throw new InvalidArgumentError( + 'Command kill scope must be one of: process, group.' + ) + } +} diff --git a/packages/js-sdk/src/sandbox/commands/pty.ts b/packages/js-sdk/src/sandbox/commands/pty.ts index 0fb685a615..ec0a216a93 100644 --- a/packages/js-sdk/src/sandbox/commands/pty.ts +++ b/packages/js-sdk/src/sandbox/commands/pty.ts @@ -5,6 +5,7 @@ import { Client, Transport, } from '@connectrpc/connect' +import { compareVersions } from 'compare-versions' import { Signal, @@ -19,6 +20,10 @@ import { setupRequestController, } from '../../connectionConfig' import { CommandHandle } from './commandHandle' +import type { CommandKillOpts } from '.' +import { validateCommandKillScope } from './kill' +import { ENVD_COMMANDS_DESCENDANTS } from '../../envd/versions' +import { SandboxError } from '../../errors' import { authenticationHeader, handleRpcErrorWithHealthCheck, @@ -148,7 +153,7 @@ export class Pty { return new CommandHandle( pid, cleanup, - () => this.kill(pid), + (killOpts) => this.kill(pid, killOpts), events, undefined, undefined, @@ -205,7 +210,7 @@ export class Pty { return new CommandHandle( pid, cleanup, - () => this.kill(pid), + (killOpts) => this.kill(pid, killOpts), events, undefined, undefined, @@ -306,14 +311,21 @@ export class Pty { * It uses `SIGKILL` signal to kill the PTY. * * @param pid process ID of the PTY. - * @param opts connection options. + * @param opts kill and connection options. * * @returns `true` if the PTY was killed, `false` if the PTY was not found. */ - async kill( - pid: number, - opts?: Pick - ): Promise { + async kill(pid: number, opts?: CommandKillOpts): Promise { + validateCommandKillScope(opts?.scope) + if ( + opts?.scope === 'group' && + compareVersions(this.envdVersion, ENVD_COMMANDS_DESCENDANTS) < 0 + ) { + throw new SandboxError( + `Sandbox envd version ${this.envdVersion} doesn't support group-scoped command termination. Please rebuild your template to pick up the latest sandbox version.` + ) + } + try { await this.rpc.sendSignal( { @@ -324,6 +336,7 @@ export class Pty { }, }, signal: Signal.SIGKILL, + descendants: opts?.scope === 'group', }, { signal: this.connectionConfig.getSignal( diff --git a/packages/js-sdk/tests/sandbox/commands/commandHandle.test.ts b/packages/js-sdk/tests/sandbox/commands/commandHandle.test.ts index eec91b7b5a..957a74981a 100644 --- a/packages/js-sdk/tests/sandbox/commands/commandHandle.test.ts +++ b/packages/js-sdk/tests/sandbox/commands/commandHandle.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it, vi } from 'vitest' import { CommandHandle } from '../../../src/sandbox/commands/commandHandle' +import { Commands, Pty } from '../../../src/sandbox/commands' +import { InvalidArgumentError } from '../../../src/errors' type EventKind = 'stdout' | 'stderr' | 'pty' @@ -131,6 +133,101 @@ function createControllableEvents() { } describe('CommandHandle', () => { + it('sends descendant scope through the Commands RPC', async () => { + const sendSignal = vi.fn(async () => ({})) + const commands = Object.create(Commands.prototype) as Commands + Object.assign(commands, { + rpc: { sendSignal }, + connectionConfig: { getSignal: () => undefined }, + envdVersion: '0.7.1', + }) + + await expect(commands.kill(42, { scope: 'group' })).resolves.toBe(true) + expect(sendSignal).toHaveBeenCalledWith( + expect.objectContaining({ descendants: true }), + expect.anything() + ) + }) + + it('rejects descendant scope when envd is too old', async () => { + const sendSignal = vi.fn(async () => ({})) + const commands = Object.create(Commands.prototype) as Commands + Object.assign(commands, { + rpc: { sendSignal }, + connectionConfig: { getSignal: () => undefined }, + envdVersion: '0.7.0', + }) + + await expect(commands.kill(42, { scope: 'group' })).rejects.toThrow( + "doesn't support group-scoped command termination" + ) + expect(sendSignal).not.toHaveBeenCalled() + }) + + it('sends group scope through the PTY RPC', async () => { + const sendSignal = vi.fn(async () => ({})) + const pty = Object.create(Pty.prototype) as Pty + Object.assign(pty, { + rpc: { sendSignal }, + connectionConfig: { getSignal: () => undefined }, + envdVersion: '0.7.1', + }) + + await expect(pty.kill(43, { scope: 'group' })).resolves.toBe(true) + expect(sendSignal).toHaveBeenCalledWith( + expect.objectContaining({ descendants: true }), + expect.anything() + ) + }) + + it('rejects an invalid command kill scope before sending the RPC', async () => { + const sendSignal = vi.fn(async () => ({})) + const commands = Object.create(Commands.prototype) as Commands + Object.assign(commands, { + rpc: { sendSignal }, + connectionConfig: { getSignal: () => undefined }, + envdVersion: '0.7.1', + }) + + await expect(commands.kill(42, { scope: 'groups' as any })).rejects.toThrow( + InvalidArgumentError + ) + expect(sendSignal).not.toHaveBeenCalled() + }) + + it('rejects an invalid PTY kill scope before sending the RPC', async () => { + const sendSignal = vi.fn(async () => ({})) + const pty = Object.create(Pty.prototype) as Pty + Object.assign(pty, { + rpc: { sendSignal }, + connectionConfig: { getSignal: () => undefined }, + envdVersion: '0.7.1', + }) + + await expect(pty.kill(43, { scope: 'groups' as any })).rejects.toThrow( + InvalidArgumentError + ) + expect(sendSignal).not.toHaveBeenCalled() + }) + + it('forwards descendant scope when killing', async () => { + const handleKill = vi.fn(async () => true) + const handle = new CommandHandle( + 1, + () => {}, + handleKill, + createEvents('stdout') + ) + + await expect( + handle.kill({ scope: 'group', requestTimeoutMs: 500 }) + ).resolves.toBe(true) + expect(handleKill).toHaveBeenCalledWith({ + scope: 'group', + requestTimeoutMs: 500, + }) + }) + it.each(['stdout', 'stderr', 'pty'])( 'wait awaits async %s callbacks', async (kind) => { diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index 1d3cac9f4d..28e846526c 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -59,6 +59,7 @@ ) from .sandbox.commands.command_handle import ( CommandExitException, + CommandKillScope, CommandResult, PtyOutput, PtySize, @@ -205,6 +206,7 @@ "GitFileStatus", "GitResetMode", # Command handle + "CommandKillScope", "CommandResult", "Stderr", "Stdout", diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index 135420b696..58f31ca0cc 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -10,7 +10,6 @@ from .build_log_entry import BuildLogEntry from .build_status_reason import BuildStatusReason from .connect_sandbox import ConnectSandbox -from .created_access_token import CreatedAccessToken from .created_team_api_key import CreatedTeamAPIKey from .delete_template_tags_request import DeleteTemplateTagsRequest from .disk_metrics import DiskMetrics @@ -27,7 +26,6 @@ from .machine_info import MachineInfo from .max_team_metric import MaxTeamMetric from .mcp_type_0 import McpType0 -from .new_access_token import NewAccessToken from .new_sandbox import NewSandbox from .new_secret import NewSecret from .new_team_api_key import NewTeamAPIKey @@ -39,6 +37,10 @@ from .node_status_change import NodeStatusChange from .order_direction import OrderDirection from .resumed_sandbox import ResumedSandbox +from .rig import Rig +from .rig_capacity_change import RigCapacityChange +from .rig_error import RigError +from .rig_instance import RigInstance from .sandbox import Sandbox from .sandbox_auto_resume_config import SandboxAutoResumeConfig from .sandbox_detail import SandboxDetail @@ -112,7 +114,6 @@ "BuildLogEntry", "BuildStatusReason", "ConnectSandbox", - "CreatedAccessToken", "CreatedTeamAPIKey", "DeleteTemplateTagsRequest", "DiskMetrics", @@ -129,7 +130,6 @@ "MachineInfo", "MaxTeamMetric", "McpType0", - "NewAccessToken", "NewSandbox", "NewSecret", "NewTeamAPIKey", @@ -141,6 +141,10 @@ "NodeStatusChange", "OrderDirection", "ResumedSandbox", + "Rig", + "RigCapacityChange", + "RigError", + "RigInstance", "Sandbox", "SandboxAutoResumeConfig", "SandboxDetail", diff --git a/packages/python-sdk/e2b/api/client/models/created_access_token.py b/packages/python-sdk/e2b/api/client/models/created_access_token.py deleted file mode 100644 index 2b038a2d72..0000000000 --- a/packages/python-sdk/e2b/api/client/models/created_access_token.py +++ /dev/null @@ -1,100 +0,0 @@ -import datetime -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, TypeVar -from uuid import UUID - -from attrs import define as _attrs_define -from attrs import field as _attrs_field -from dateutil.parser import isoparse - -if TYPE_CHECKING: - from ..models.identifier_masking_details import IdentifierMaskingDetails - - -T = TypeVar("T", bound="CreatedAccessToken") - - -@_attrs_define -class CreatedAccessToken: - """ - Attributes: - id (UUID): Identifier of the access token - name (str): Name of the access token - token (str): The fully created access token - mask (IdentifierMaskingDetails): - created_at (datetime.datetime): Timestamp of access token creation - """ - - id: UUID - name: str - token: str - mask: "IdentifierMaskingDetails" - created_at: datetime.datetime - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) - - def to_dict(self) -> dict[str, Any]: - id = str(self.id) - - name = self.name - - token = self.token - - mask = self.mask.to_dict() - - created_at = self.created_at.isoformat() - - field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) - field_dict.update( - { - "id": id, - "name": name, - "token": token, - "mask": mask, - "createdAt": created_at, - } - ) - - return field_dict - - @classmethod - def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - from ..models.identifier_masking_details import IdentifierMaskingDetails - - d = dict(src_dict) - id = UUID(d.pop("id")) - - name = d.pop("name") - - token = d.pop("token") - - mask = IdentifierMaskingDetails.from_dict(d.pop("mask")) - - created_at = isoparse(d.pop("createdAt")) - - created_access_token = cls( - id=id, - name=name, - token=token, - mask=mask, - created_at=created_at, - ) - - created_access_token.additional_properties = d - return created_access_token - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/rig.py b/packages/python-sdk/e2b/api/client/models/rig.py new file mode 100644 index 0000000000..537dc8fd6d --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/rig.py @@ -0,0 +1,115 @@ +from collections.abc import Mapping +from typing import Any, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Rig") + + +@_attrs_define +class Rig: + """An orchestrator node pool backed by one cloud scaling group + + Attributes: + id (str): Rig identifier (e.g. "default") + provider (str): Cloud provider backing the rig ("aws" or "gcp") + resource_id (str): Canonical cloud resource ID of the scaling group backing the rig (ARN on AWS, self-link on + GCP) + capacity_desired (int): Desired number of instances in the rig + capacity_current (int): Number of instances currently attached to the rig + capacity_min (Union[Unset, int]): Minimum capacity enforced on the rig's scaling group. Omitted when nothing + enforces bounds (GCP MIG without an active autoscaler). + capacity_max (Union[Unset, int]): Maximum capacity enforced on the rig's scaling group. Omitted when nothing + enforces bounds (GCP MIG without an active autoscaler). + """ + + id: str + provider: str + resource_id: str + capacity_desired: int + capacity_current: int + capacity_min: Union[Unset, int] = UNSET + capacity_max: Union[Unset, int] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + provider = self.provider + + resource_id = self.resource_id + + capacity_desired = self.capacity_desired + + capacity_current = self.capacity_current + + capacity_min = self.capacity_min + + capacity_max = self.capacity_max + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "provider": provider, + "resourceID": resource_id, + "capacityDesired": capacity_desired, + "capacityCurrent": capacity_current, + } + ) + if capacity_min is not UNSET: + field_dict["capacityMin"] = capacity_min + if capacity_max is not UNSET: + field_dict["capacityMax"] = capacity_max + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + id = d.pop("id") + + provider = d.pop("provider") + + resource_id = d.pop("resourceID") + + capacity_desired = d.pop("capacityDesired") + + capacity_current = d.pop("capacityCurrent") + + capacity_min = d.pop("capacityMin", UNSET) + + capacity_max = d.pop("capacityMax", UNSET) + + rig = cls( + id=id, + provider=provider, + resource_id=resource_id, + capacity_desired=capacity_desired, + capacity_current=capacity_current, + capacity_min=capacity_min, + capacity_max=capacity_max, + ) + + rig.additional_properties = d + return rig + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/new_access_token.py b/packages/python-sdk/e2b/api/client/models/rig_capacity_change.py similarity index 70% rename from packages/python-sdk/e2b/api/client/models/new_access_token.py rename to packages/python-sdk/e2b/api/client/models/rig_capacity_change.py index 642dac80d4..7854ff23e0 100644 --- a/packages/python-sdk/e2b/api/client/models/new_access_token.py +++ b/packages/python-sdk/e2b/api/client/models/rig_capacity_change.py @@ -4,27 +4,28 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field -T = TypeVar("T", bound="NewAccessToken") +T = TypeVar("T", bound="RigCapacityChange") @_attrs_define -class NewAccessToken: - """ +class RigCapacityChange: + """Desired capacity to set on the rig's scaling group + Attributes: - name (str): Name of the access token + desired (int): Absolute desired number of instances in the rig """ - name: str + desired: int additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - name = self.name + desired = self.desired field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { - "name": name, + "desired": desired, } ) @@ -33,14 +34,14 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) - name = d.pop("name") + desired = d.pop("desired") - new_access_token = cls( - name=name, + rig_capacity_change = cls( + desired=desired, ) - new_access_token.additional_properties = d - return new_access_token + rig_capacity_change.additional_properties = d + return rig_capacity_change @property def additional_keys(self) -> list[str]: diff --git a/packages/python-sdk/e2b/api/client/models/rig_error.py b/packages/python-sdk/e2b/api/client/models/rig_error.py new file mode 100644 index 0000000000..5a098b2606 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/rig_error.py @@ -0,0 +1,98 @@ +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="RigError") + + +@_attrs_define +class RigError: + """Scaling error on the rig's scaling group, e.g. a failed instance creation due to resource exhaustion + + Attributes: + timestamp (datetime.datetime): When the error occurred + code (str): Provider-specific error code (e.g. ZONE_RESOURCE_POOL_EXHAUSTED, Failed) + message (str): Human-readable error message + instance (Union[Unset, str]): Instance the error relates to, if any + action (Union[Unset, str]): Action being performed when the error occurred (e.g. CREATING) + """ + + timestamp: datetime.datetime + code: str + message: str + instance: Union[Unset, str] = UNSET + action: Union[Unset, str] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + timestamp = self.timestamp.isoformat() + + code = self.code + + message = self.message + + instance = self.instance + + action = self.action + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "timestamp": timestamp, + "code": code, + "message": message, + } + ) + if instance is not UNSET: + field_dict["instance"] = instance + if action is not UNSET: + field_dict["action"] = action + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + timestamp = isoparse(d.pop("timestamp")) + + code = d.pop("code") + + message = d.pop("message") + + instance = d.pop("instance", UNSET) + + action = d.pop("action", UNSET) + + rig_error = cls( + timestamp=timestamp, + code=code, + message=message, + instance=instance, + action=action, + ) + + rig_error.additional_properties = d + return rig_error + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/rig_instance.py b/packages/python-sdk/e2b/api/client/models/rig_instance.py new file mode 100644 index 0000000000..98cd9811c8 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/rig_instance.py @@ -0,0 +1,98 @@ +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="RigInstance") + + +@_attrs_define +class RigInstance: + """An instance attached to a rig's scaling group + + Attributes: + id (str): Provider instance ID (EC2 instance ID on AWS, instance name on GCP), also the node ID the orchestrator + reports + transitioning (bool): The provider is creating, deleting, recreating or otherwise mutating the instance + terminating (bool): The instance is on its way out of the group and can never become healthy again + created_at (Union[Unset, datetime.datetime]): When the provider created the instance. Omitted while the instance + is transitioning. + """ + + id: str + transitioning: bool + terminating: bool + created_at: Union[Unset, datetime.datetime] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + transitioning = self.transitioning + + terminating = self.terminating + + created_at: Union[Unset, str] = UNSET + if not isinstance(self.created_at, Unset): + created_at = self.created_at.isoformat() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "transitioning": transitioning, + "terminating": terminating, + } + ) + if created_at is not UNSET: + field_dict["createdAt"] = created_at + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + id = d.pop("id") + + transitioning = d.pop("transitioning") + + terminating = d.pop("terminating") + + _created_at = d.pop("createdAt", UNSET) + created_at: Union[Unset, datetime.datetime] + if isinstance(_created_at, Unset): + created_at = UNSET + else: + created_at = isoparse(_created_at) + + rig_instance = cls( + id=id, + transitioning=transitioning, + terminating=terminating, + created_at=created_at, + ) + + rig_instance.additional_properties = d + return rig_instance + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_network_config.py b/packages/python-sdk/e2b/api/client/models/sandbox_network_config.py index 970666efef..108a93b2d4 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_network_config.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_network_config.py @@ -31,9 +31,16 @@ class SandboxNetworkConfig: TCP is tunneled through the proxy after allow/deny filtering; the sandbox is unaware. Domain-matched flows use remote DNS (ATYP=domain). mask_request_host (Union[Unset, str]): Specify host mask which will be used for all sandbox requests - rules (Union[Unset, SandboxNetworkConfigRules]): Per-domain transform rules applied to matching egress - HTTP/HTTPS requests. Keys are domains (e.g. "api.example.com", "example.com"). A domain listed here is not - automatically allowed - use allowOut to permit the traffic. + https_ports (Union[Unset, list[int]]): Sandbox ports that serve HTTPS rather than plaintext HTTP. Affects how + the proxy reaches the service inside the sandbox; the public URL is HTTPS either way. Certificates are not + verified, so self-signed ones work. The envd port (49983) cannot be listed. + rules (Union[Unset, SandboxNetworkConfigRules]): Per-domain transform rules applied to matching outbound HTTPS + requests. Keys may be exact DNS names (for example, "api.example.com") or a leading wildcard (for example, + "*.example.com"), and are normalized to lowercase on write. Wildcards match subdomains at any depth but not the + apex domain; a bare "*" is invalid. Exact rules take precedence, followed by the longest matching wildcard + suffix, and matching rule sets are not merged. Broad wildcards such as "*.com" are allowed and may expose + transformed credentials to every matching destination the sandbox contacts. Rules do not grant network access; + configure allowOut separately to permit the destination. """ allow_public_traffic: Union[Unset, bool] = True @@ -41,6 +48,7 @@ class SandboxNetworkConfig: deny_out: Union[Unset, list[str]] = UNSET egress_proxy: Union["SandboxEgressProxyConfigType0", None, Unset] = UNSET mask_request_host: Union[Unset, str] = UNSET + https_ports: Union[Unset, list[int]] = UNSET rules: Union[Unset, "SandboxNetworkConfigRules"] = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -69,6 +77,10 @@ def to_dict(self) -> dict[str, Any]: mask_request_host = self.mask_request_host + https_ports: Union[Unset, list[int]] = UNSET + if not isinstance(self.https_ports, Unset): + https_ports = self.https_ports + rules: Union[Unset, dict[str, Any]] = UNSET if not isinstance(self.rules, Unset): rules = self.rules.to_dict() @@ -86,6 +98,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["egressProxy"] = egress_proxy if mask_request_host is not UNSET: field_dict["maskRequestHost"] = mask_request_host + if https_ports is not UNSET: + field_dict["httpsPorts"] = https_ports if rules is not UNSET: field_dict["rules"] = rules @@ -128,6 +142,8 @@ def _parse_egress_proxy( mask_request_host = d.pop("maskRequestHost", UNSET) + https_ports = cast(list[int], d.pop("httpsPorts", UNSET)) + _rules = d.pop("rules", UNSET) rules: Union[Unset, SandboxNetworkConfigRules] if isinstance(_rules, Unset): @@ -141,6 +157,7 @@ def _parse_egress_proxy( deny_out=deny_out, egress_proxy=egress_proxy, mask_request_host=mask_request_host, + https_ports=https_ports, rules=rules, ) diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_network_config_rules.py b/packages/python-sdk/e2b/api/client/models/sandbox_network_config_rules.py index aeece3851b..e887c7b608 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_network_config_rules.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_network_config_rules.py @@ -13,8 +13,12 @@ @_attrs_define class SandboxNetworkConfigRules: - """Per-domain transform rules applied to matching egress HTTP/HTTPS requests. Keys are domains (e.g. "api.example.com", - "example.com"). A domain listed here is not automatically allowed - use allowOut to permit the traffic. + """Per-domain transform rules applied to matching outbound HTTPS requests. Keys may be exact DNS names (for example, + "api.example.com") or a leading wildcard (for example, "*.example.com"), and are normalized to lowercase on write. + Wildcards match subdomains at any depth but not the apex domain; a bare "*" is invalid. Exact rules take precedence, + followed by the longest matching wildcard suffix, and matching rule sets are not merged. Broad wildcards such as + "*.com" are allowed and may expose transformed credentials to every matching destination the sandbox contacts. Rules + do not grant network access; configure allowOut separately to permit the destination. """ diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config.py b/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config.py index 1a47ddc7f6..3ea8371d83 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config.py @@ -32,8 +32,13 @@ class SandboxNetworkUpdateConfig: egress_proxy (Union['SandboxEgressProxyConfigType0', None, Unset]): SOCKS5 proxy for sandbox egress. Outbound TCP is tunneled through the proxy after allow/deny filtering; the sandbox is unaware. Domain-matched flows use remote DNS (ATYP=domain). - rules (Union[Unset, SandboxNetworkUpdateConfigRules]): Per-domain transform rules. Replaces all existing rules - when provided. + rules (Union[Unset, SandboxNetworkUpdateConfigRules]): Per-domain transform rules applied to matching outbound + HTTPS requests. Replaces all existing rules when provided. Keys may be exact DNS names or a single leading + wildcard (for example, "*.example.com"), and are normalized to lowercase on write. Wildcards match subdomains at + any depth but not the apex domain; a bare "*" is invalid. Exact rules take precedence, followed by the longest + matching wildcard suffix, and matching rule sets are not merged. Broad wildcards such as "*.com" are allowed and + may expose transformed credentials to every matching destination the sandbox contacts. Rules do not grant + network access; configure allowOut separately to permit the destination. allow_internet_access (Union[Unset, bool]): Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. """ diff --git a/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config_rules.py b/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config_rules.py index 58d268a27d..0079742a87 100644 --- a/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config_rules.py +++ b/packages/python-sdk/e2b/api/client/models/sandbox_network_update_config_rules.py @@ -13,7 +13,14 @@ @_attrs_define class SandboxNetworkUpdateConfigRules: - """Per-domain transform rules. Replaces all existing rules when provided.""" + """Per-domain transform rules applied to matching outbound HTTPS requests. Replaces all existing rules when provided. + Keys may be exact DNS names or a single leading wildcard (for example, "*.example.com"), and are normalized to + lowercase on write. Wildcards match subdomains at any depth but not the apex domain; a bare "*" is invalid. Exact + rules take precedence, followed by the longest matching wildcard suffix, and matching rule sets are not merged. + Broad wildcards such as "*.com" are allowed and may expose transformed credentials to every matching destination the + sandbox contacts. Rules do not grant network access; configure allowOut separately to permit the destination. + + """ additional_properties: dict[str, list["SandboxNetworkRule"]] = _attrs_field( init=False, factory=dict diff --git a/packages/python-sdk/e2b/envd/process/process_pb.py b/packages/python-sdk/e2b/envd/process/process_pb.py index 588ac83a05..bcf45a84f0 100644 --- a/packages/python-sdk/e2b/envd/process/process_pb.py +++ b/packages/python-sdk/e2b/envd/process/process_pb.py @@ -714,7 +714,7 @@ def __init__( ) -> None: pass -_SendSignalRequestFields: TypeAlias = Literal["process", "signal"] +_SendSignalRequestFields: TypeAlias = Literal["process", "signal", "descendants"] class SendSignalRequest(Message[_SendSignalRequestFields]): """ @@ -731,9 +731,16 @@ class SendSignalRequest(Message[_SendSignalRequestFields]): ```proto process.Signal signal = 2; ``` + descendants: + Signal the command's process group instead of only its leader. + This includes descendants that remain in the command's process group. + + ```proto + bool descendants = 3; + ``` """ - __slots__ = ("process", "signal") + __slots__ = ("process", "signal", "descendants") if TYPE_CHECKING: @@ -742,11 +749,13 @@ def __init__( *, process: ProcessSelector | None = None, signal: Signal | None = None, + descendants: bool = False, ) -> None: pass process: ProcessSelector | None signal: Signal + descendants: bool _SendSignalResponseFields: TypeAlias = NoReturn @@ -895,7 +904,7 @@ class Signal(Enum): _DESC = file_desc( - b'\n\x15process/process.proto\x12\x07process"\\\n\x03PTY\x12%\n\x04size\x18\x01 \x01(\x0b2\x11.process.PTY.SizeR\x04size\x1a.\n\x04Size\x12\x12\n\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n\x04rows\x18\x02 \x01(\rR\x04rows"\xc3\x01\n\rProcessConfig\x12\x10\n\x03cmd\x18\x01 \x01(\tR\x03cmd\x12\x12\n\x04args\x18\x02 \x03(\tR\x04args\x124\n\x04envs\x18\x03 \x03(\x0b2 .process.ProcessConfig.EnvsEntryR\x04envs\x12\x15\n\x03cwd\x18\x04 \x01(\tH\x00R\x03cwd\x88\x01\x01\x1a7\n\tEnvsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n\x04_cwd"\r\n\x0bListRequest"n\n\x0bProcessInfo\x12.\n\x06config\x18\x01 \x01(\x0b2\x16.process.ProcessConfigR\x06config\x12\x10\n\x03pid\x18\x02 \x01(\rR\x03pid\x12\x15\n\x03tag\x18\x03 \x01(\tH\x00R\x03tag\x88\x01\x01B\x06\n\x04_tag"B\n\x0cListResponse\x122\n\tprocesses\x18\x01 \x03(\x0b2\x14.process.ProcessInfoR\tprocesses"\xb1\x01\n\x0cStartRequest\x120\n\x07process\x18\x01 \x01(\x0b2\x16.process.ProcessConfigR\x07process\x12#\n\x03pty\x18\x02 \x01(\x0b2\x0c.process.PTYH\x00R\x03pty\x88\x01\x01\x12\x15\n\x03tag\x18\x03 \x01(\tH\x01R\x03tag\x88\x01\x01\x12\x19\n\x05stdin\x18\x04 \x01(\x08H\x02R\x05stdin\x88\x01\x01B\x06\n\x04_ptyB\x06\n\x04_tagB\x08\n\x06_stdin"p\n\rUpdateRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x12#\n\x03pty\x18\x02 \x01(\x0b2\x0c.process.PTYH\x00R\x03pty\x88\x01\x01B\x06\n\x04_pty"\x10\n\x0eUpdateResponse"\x87\x04\n\x0cProcessEvent\x128\n\x05start\x18\x01 \x01(\x0b2 .process.ProcessEvent.StartEventH\x00R\x05start\x125\n\x04data\x18\x02 \x01(\x0b2\x1f.process.ProcessEvent.DataEventH\x00R\x04data\x122\n\x03end\x18\x03 \x01(\x0b2\x1e.process.ProcessEvent.EndEventH\x00R\x03end\x12?\n\tkeepalive\x18\x04 \x01(\x0b2\x1f.process.ProcessEvent.KeepAliveH\x00R\tkeepalive\x1a\x1e\n\nStartEvent\x12\x10\n\x03pid\x18\x01 \x01(\rR\x03pid\x1a]\n\tDataEvent\x12\x18\n\x06stdout\x18\x01 \x01(\x0cH\x00R\x06stdout\x12\x18\n\x06stderr\x18\x02 \x01(\x0cH\x00R\x06stderr\x12\x12\n\x03pty\x18\x03 \x01(\x0cH\x00R\x03ptyB\x08\n\x06output\x1a|\n\x08EndEvent\x12\x1b\n\texit_code\x18\x01 \x01(\x11R\x08exitCode\x12\x16\n\x06exited\x18\x02 \x01(\x08R\x06exited\x12\x16\n\x06status\x18\x03 \x01(\tR\x06status\x12\x19\n\x05error\x18\x04 \x01(\tH\x00R\x05error\x88\x01\x01B\x08\n\x06_error\x1a\x0b\n\tKeepAliveB\x07\n\x05event"<\n\rStartResponse\x12+\n\x05event\x18\x01 \x01(\x0b2\x15.process.ProcessEventR\x05event">\n\x0fConnectResponse\x12+\n\x05event\x18\x01 \x01(\x0b2\x15.process.ProcessEventR\x05event"s\n\x10SendInputRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x12+\n\x05input\x18\x02 \x01(\x0b2\x15.process.ProcessInputR\x05input"\x13\n\x11SendInputResponse"C\n\x0cProcessInput\x12\x16\n\x05stdin\x18\x01 \x01(\x0cH\x00R\x05stdin\x12\x12\n\x03pty\x18\x02 \x01(\x0cH\x00R\x03ptyB\x07\n\x05input"\xea\x02\n\x12StreamInputRequest\x12>\n\x05start\x18\x01 \x01(\x0b2&.process.StreamInputRequest.StartEventH\x00R\x05start\x12;\n\x04data\x18\x02 \x01(\x0b2%.process.StreamInputRequest.DataEventH\x00R\x04data\x12E\n\tkeepalive\x18\x03 \x01(\x0b2%.process.StreamInputRequest.KeepAliveH\x00R\tkeepalive\x1a@\n\nStartEvent\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x1a8\n\tDataEvent\x12+\n\x05input\x18\x02 \x01(\x0b2\x15.process.ProcessInputR\x05input\x1a\x0b\n\tKeepAliveB\x07\n\x05event"\x15\n\x13StreamInputResponse"p\n\x11SendSignalRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x12\'\n\x06signal\x18\x02 \x01(\x0e2\x0f.process.SignalR\x06signal"\x14\n\x12SendSignalResponse"G\n\x11CloseStdinRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process"\x14\n\x12CloseStdinResponse"D\n\x0eConnectRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process"E\n\x0fProcessSelector\x12\x12\n\x03pid\x18\x01 \x01(\rH\x00R\x03pid\x12\x12\n\x03tag\x18\x02 \x01(\tH\x00R\x03tagB\n\n\x08selector*H\n\x06Signal\x12\x16\n\x12SIGNAL_UNSPECIFIED\x10\x00\x12\x12\n\x0eSIGNAL_SIGTERM\x10\x0f\x12\x12\n\x0eSIGNAL_SIGKILL\x10\t2\x91\x04\n\x07Process\x123\n\x04List\x12\x14.process.ListRequest\x1a\x15.process.ListResponse\x12>\n\x07Connect\x12\x17.process.ConnectRequest\x1a\x18.process.ConnectResponse0\x01\x128\n\x05Start\x12\x15.process.StartRequest\x1a\x16.process.StartResponse0\x01\x129\n\x06Update\x12\x16.process.UpdateRequest\x1a\x17.process.UpdateResponse\x12J\n\x0bStreamInput\x12\x1b.process.StreamInputRequest\x1a\x1c.process.StreamInputResponse(\x01\x12B\n\tSendInput\x12\x19.process.SendInputRequest\x1a\x1a.process.SendInputResponse\x12E\n\nSendSignal\x12\x1a.process.SendSignalRequest\x1a\x1b.process.SendSignalResponse\x12E\n\nCloseStdin\x12\x1a.process.CloseStdinRequest\x1a\x1b.process.CloseStdinResponseBW\n\x0bcom.processB\x0cProcessProtoP\x01\xa2\x02\x03PXX\xaa\x02\x07Process\xca\x02\x07Process\xe2\x02\x13Process\\GPBMetadata\xea\x02\x07Processb\x06proto3', + b'\n\x15process/process.proto\x12\x07process"\\\n\x03PTY\x12%\n\x04size\x18\x01 \x01(\x0b2\x11.process.PTY.SizeR\x04size\x1a.\n\x04Size\x12\x12\n\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n\x04rows\x18\x02 \x01(\rR\x04rows"\xc3\x01\n\rProcessConfig\x12\x10\n\x03cmd\x18\x01 \x01(\tR\x03cmd\x12\x12\n\x04args\x18\x02 \x03(\tR\x04args\x124\n\x04envs\x18\x03 \x03(\x0b2 .process.ProcessConfig.EnvsEntryR\x04envs\x12\x15\n\x03cwd\x18\x04 \x01(\tH\x00R\x03cwd\x88\x01\x01\x1a7\n\tEnvsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n\x04_cwd"\r\n\x0bListRequest"n\n\x0bProcessInfo\x12.\n\x06config\x18\x01 \x01(\x0b2\x16.process.ProcessConfigR\x06config\x12\x10\n\x03pid\x18\x02 \x01(\rR\x03pid\x12\x15\n\x03tag\x18\x03 \x01(\tH\x00R\x03tag\x88\x01\x01B\x06\n\x04_tag"B\n\x0cListResponse\x122\n\tprocesses\x18\x01 \x03(\x0b2\x14.process.ProcessInfoR\tprocesses"\xb1\x01\n\x0cStartRequest\x120\n\x07process\x18\x01 \x01(\x0b2\x16.process.ProcessConfigR\x07process\x12#\n\x03pty\x18\x02 \x01(\x0b2\x0c.process.PTYH\x00R\x03pty\x88\x01\x01\x12\x15\n\x03tag\x18\x03 \x01(\tH\x01R\x03tag\x88\x01\x01\x12\x19\n\x05stdin\x18\x04 \x01(\x08H\x02R\x05stdin\x88\x01\x01B\x06\n\x04_ptyB\x06\n\x04_tagB\x08\n\x06_stdin"p\n\rUpdateRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x12#\n\x03pty\x18\x02 \x01(\x0b2\x0c.process.PTYH\x00R\x03pty\x88\x01\x01B\x06\n\x04_pty"\x10\n\x0eUpdateResponse"\x87\x04\n\x0cProcessEvent\x128\n\x05start\x18\x01 \x01(\x0b2 .process.ProcessEvent.StartEventH\x00R\x05start\x125\n\x04data\x18\x02 \x01(\x0b2\x1f.process.ProcessEvent.DataEventH\x00R\x04data\x122\n\x03end\x18\x03 \x01(\x0b2\x1e.process.ProcessEvent.EndEventH\x00R\x03end\x12?\n\tkeepalive\x18\x04 \x01(\x0b2\x1f.process.ProcessEvent.KeepAliveH\x00R\tkeepalive\x1a\x1e\n\nStartEvent\x12\x10\n\x03pid\x18\x01 \x01(\rR\x03pid\x1a]\n\tDataEvent\x12\x18\n\x06stdout\x18\x01 \x01(\x0cH\x00R\x06stdout\x12\x18\n\x06stderr\x18\x02 \x01(\x0cH\x00R\x06stderr\x12\x12\n\x03pty\x18\x03 \x01(\x0cH\x00R\x03ptyB\x08\n\x06output\x1a|\n\x08EndEvent\x12\x1b\n\texit_code\x18\x01 \x01(\x11R\x08exitCode\x12\x16\n\x06exited\x18\x02 \x01(\x08R\x06exited\x12\x16\n\x06status\x18\x03 \x01(\tR\x06status\x12\x19\n\x05error\x18\x04 \x01(\tH\x00R\x05error\x88\x01\x01B\x08\n\x06_error\x1a\x0b\n\tKeepAliveB\x07\n\x05event"<\n\rStartResponse\x12+\n\x05event\x18\x01 \x01(\x0b2\x15.process.ProcessEventR\x05event">\n\x0fConnectResponse\x12+\n\x05event\x18\x01 \x01(\x0b2\x15.process.ProcessEventR\x05event"s\n\x10SendInputRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x12+\n\x05input\x18\x02 \x01(\x0b2\x15.process.ProcessInputR\x05input"\x13\n\x11SendInputResponse"C\n\x0cProcessInput\x12\x16\n\x05stdin\x18\x01 \x01(\x0cH\x00R\x05stdin\x12\x12\n\x03pty\x18\x02 \x01(\x0cH\x00R\x03ptyB\x07\n\x05input"\xea\x02\n\x12StreamInputRequest\x12>\n\x05start\x18\x01 \x01(\x0b2&.process.StreamInputRequest.StartEventH\x00R\x05start\x12;\n\x04data\x18\x02 \x01(\x0b2%.process.StreamInputRequest.DataEventH\x00R\x04data\x12E\n\tkeepalive\x18\x03 \x01(\x0b2%.process.StreamInputRequest.KeepAliveH\x00R\tkeepalive\x1a@\n\nStartEvent\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x1a8\n\tDataEvent\x12+\n\x05input\x18\x02 \x01(\x0b2\x15.process.ProcessInputR\x05input\x1a\x0b\n\tKeepAliveB\x07\n\x05event"\x15\n\x13StreamInputResponse"\x92\x01\n\x11SendSignalRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process\x12\'\n\x06signal\x18\x02 \x01(\x0e2\x0f.process.SignalR\x06signal\x12 \n\x0bdescendants\x18\x03 \x01(\x08R\x0bdescendants"\x14\n\x12SendSignalResponse"G\n\x11CloseStdinRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process"\x14\n\x12CloseStdinResponse"D\n\x0eConnectRequest\x122\n\x07process\x18\x01 \x01(\x0b2\x18.process.ProcessSelectorR\x07process"E\n\x0fProcessSelector\x12\x12\n\x03pid\x18\x01 \x01(\rH\x00R\x03pid\x12\x12\n\x03tag\x18\x02 \x01(\tH\x00R\x03tagB\n\n\x08selector*H\n\x06Signal\x12\x16\n\x12SIGNAL_UNSPECIFIED\x10\x00\x12\x12\n\x0eSIGNAL_SIGTERM\x10\x0f\x12\x12\n\x0eSIGNAL_SIGKILL\x10\t2\x91\x04\n\x07Process\x123\n\x04List\x12\x14.process.ListRequest\x1a\x15.process.ListResponse\x12>\n\x07Connect\x12\x17.process.ConnectRequest\x1a\x18.process.ConnectResponse0\x01\x128\n\x05Start\x12\x15.process.StartRequest\x1a\x16.process.StartResponse0\x01\x129\n\x06Update\x12\x16.process.UpdateRequest\x1a\x17.process.UpdateResponse\x12J\n\x0bStreamInput\x12\x1b.process.StreamInputRequest\x1a\x1c.process.StreamInputResponse(\x01\x12B\n\tSendInput\x12\x19.process.SendInputRequest\x1a\x1a.process.SendInputResponse\x12E\n\nSendSignal\x12\x1a.process.SendSignalRequest\x1a\x1b.process.SendSignalResponse\x12E\n\nCloseStdin\x12\x1a.process.CloseStdinRequest\x1a\x1b.process.CloseStdinResponseBW\n\x0bcom.processB\x0cProcessProtoP\x01\xa2\x02\x03PXX\xaa\x02\x07Process\xca\x02\x07Process\xe2\x02\x13Process\\GPBMetadata\xea\x02\x07Processb\x06proto3', [], { "PTY": PTY, diff --git a/packages/python-sdk/e2b/envd/versions.py b/packages/python-sdk/e2b/envd/versions.py index 2b4542ce24..7888c72124 100644 --- a/packages/python-sdk/e2b/envd/versions.py +++ b/packages/python-sdk/e2b/envd/versions.py @@ -9,3 +9,4 @@ ENVD_FILE_METADATA = Version("0.6.2") ENVD_VERSION_FS_EVENT_ENTRY_INFO = Version("0.6.3") ENVD_VERSION_WATCH_NETWORK_MOUNTS = Version("0.6.4") +ENVD_COMMANDS_DESCENDANTS = Version("0.7.1") diff --git a/packages/python-sdk/e2b/sandbox/commands/command_handle.py b/packages/python-sdk/e2b/sandbox/commands/command_handle.py index 043f871c25..42a97a92c4 100644 --- a/packages/python-sdk/e2b/sandbox/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox/commands/command_handle.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from typing import Optional +from typing import Literal, Optional -from e2b.exceptions import SandboxException +from e2b.exceptions import InvalidArgumentException, SandboxException Stdout = str """ @@ -16,6 +16,19 @@ Pty output. """ +CommandKillScope = Literal["process", "group"] +""" +Scope for command termination. ``process`` signals only the managed process; +``group`` also signals descendants that remain in its process group. +""" + + +def validate_command_kill_scope(scope: object) -> None: + if scope not in ("process", "group"): + raise InvalidArgumentException( + "Command kill scope must be one of: process, group." + ) + @dataclass class PtySize: diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command.py b/packages/python-sdk/e2b/sandbox_async/commands/command.py index 19d7e23649..1e1a47e132 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command.py @@ -21,10 +21,18 @@ timeout_to_ms, ) from e2b.envd.client_async import as_stream, create_rpc_client, first_event -from e2b.envd.versions import ENVD_COMMANDS_STDIN, ENVD_ENVD_CLOSE +from e2b.envd.versions import ( + ENVD_COMMANDS_DESCENDANTS, + ENVD_COMMANDS_STDIN, + ENVD_ENVD_CLOSE, +) from e2b.exceptions import SandboxException from e2b.sandbox.commands.main import ProcessInfo -from e2b.sandbox.commands.command_handle import CommandResult +from e2b.sandbox.commands.command_handle import ( + CommandKillScope, + CommandResult, + validate_command_kill_scope, +) from e2b.sandbox_async.commands.command_handle import AsyncCommandHandle, Stderr, Stdout from e2b.sandbox_async.utils import OutputHandler @@ -92,6 +100,8 @@ async def kill( self, pid: int, request_timeout: Optional[float] = None, + *, + scope: CommandKillScope = "process", ) -> bool: """ Kill a running command specified by its process ID. @@ -99,14 +109,23 @@ async def kill( :param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()` :param request_timeout: Timeout for the request in **seconds** + :param scope: Whether to kill only the managed process or its process group :return: `True` if the command was killed, `False` if the command was not found """ + validate_command_kill_scope(scope) + if scope == "group" and self._envd_version < ENVD_COMMANDS_DESCENDANTS: + raise SandboxException( + f"Sandbox envd version {self._envd_version} doesn't support group-scoped command termination. " + "Please rebuild your template to pick up the latest sandbox version." + ) + try: await self._rpc.send_signal( process_pb.SendSignalRequest( process=process_pb.ProcessSelector(selector=Oneof("pid", pid)), signal=process_pb.Signal.SIGKILL, + descendants=scope == "group", ), timeout_ms=timeout_to_ms( self._connection_config.get_request_timeout(request_timeout) @@ -323,7 +342,9 @@ async def _start( pid = extract_start_pid(start_event, "start process") return AsyncCommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, on_stdout=on_stdout, on_stderr=on_stderr, @@ -382,7 +403,9 @@ async def connect( pid = extract_start_pid(start_event, "connect to process") return AsyncCommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, on_stdout=on_stdout, on_stderr=on_stderr, diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py index 4168cfe63d..aa48fa773b 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py @@ -20,6 +20,7 @@ from e2b.exceptions import SandboxException from e2b.sandbox.commands.command_handle import ( CommandExitException, + CommandKillScope, CommandResult, Stderr, Stdout, @@ -81,7 +82,9 @@ def exit_code(self): def __init__( self, pid: int, - handle_kill: Callable[[], Coroutine[Any, Any, bool]], + handle_kill: Callable[ + [CommandKillScope, Optional[float]], Coroutine[Any, Any, bool] + ], events: AsyncGenerator[ Union[process_pb.StartResponse, process_pb.ConnectResponse], Any ], @@ -262,15 +265,22 @@ async def wait(self) -> CommandResult: return self._result - async def kill(self) -> bool: + async def kill( + self, + *, + scope: CommandKillScope = "process", + request_timeout: Optional[float] = None, + ) -> bool: """ Kills the command. It uses `SIGKILL` signal to kill the command + :param scope: Whether to kill only the managed process or its process group + :param request_timeout: Timeout for the request in **seconds** :return: `True` if the command was killed successfully, `False` if the command was not found """ - result = await self._handle_kill() + result = await self._handle_kill(scope, request_timeout) return result async def send_stdin( diff --git a/packages/python-sdk/e2b/sandbox_async/commands/pty.py b/packages/python-sdk/e2b/sandbox_async/commands/pty.py index dda9d2e6f3..c31bac6533 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/pty.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/pty.py @@ -16,18 +16,24 @@ ) from e2b.envd.api import acheck_sandbox_health from e2b.envd.rpc import ahandle_rpc_exception_with_health +from e2b.envd.versions import ENVD_COMMANDS_DESCENDANTS from e2b.envd.utils import ( authentication_header, extract_start_pid, timeout_to_ms, ) from e2b.envd.client_async import as_stream, create_rpc_client, first_event -from e2b.sandbox.commands.command_handle import PtySize +from e2b.sandbox.commands.command_handle import ( + CommandKillScope, + PtySize, + validate_command_kill_scope, +) from e2b.sandbox_async.commands.command_handle import ( AsyncCommandHandle, OutputHandler, PtyOutput, ) +from e2b.exceptions import SandboxException class Pty: @@ -58,20 +64,31 @@ async def kill( self, pid: int, request_timeout: Optional[float] = None, + *, + scope: CommandKillScope = "process", ) -> bool: """ Kill PTY. :param pid: Process ID of the PTY :param request_timeout: Timeout for the request in **seconds** + :param scope: Whether to kill only the managed PTY process or its process group :return: `true` if the PTY was killed, `false` if the PTY was not found """ + validate_command_kill_scope(scope) + if scope == "group" and self._envd_version < ENVD_COMMANDS_DESCENDANTS: + raise SandboxException( + f"Sandbox envd version {self._envd_version} doesn't support group-scoped command termination. " + "Please rebuild your template to pick up the latest sandbox version." + ) + try: await self._rpc.send_signal( process_pb.SendSignalRequest( process=process_pb.ProcessSelector(selector=Oneof("pid", pid)), signal=process_pb.Signal.SIGKILL, + descendants=scope == "group", ), timeout_ms=timeout_to_ms( self._connection_config.get_request_timeout(request_timeout) @@ -168,7 +185,9 @@ async def create( pid = extract_start_pid(start_event, "start process") return AsyncCommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, on_pty=on_data, check_health=self._check_health, @@ -217,7 +236,9 @@ async def connect( pid = extract_start_pid(start_event, "connect to process") return AsyncCommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, on_pty=on_data, check_health=self._check_health, diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command.py b/packages/python-sdk/e2b/sandbox_sync/commands/command.py index 0198c42c5a..718dc63d54 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command.py @@ -21,10 +21,18 @@ timeout_to_ms, ) from e2b.envd.client_sync import as_stream, create_rpc_client -from e2b.envd.versions import ENVD_COMMANDS_STDIN, ENVD_ENVD_CLOSE +from e2b.envd.versions import ( + ENVD_COMMANDS_DESCENDANTS, + ENVD_COMMANDS_STDIN, + ENVD_ENVD_CLOSE, +) from e2b.exceptions import SandboxException from e2b.sandbox.commands.main import ProcessInfo -from e2b.sandbox.commands.command_handle import CommandResult +from e2b.sandbox.commands.command_handle import ( + CommandKillScope, + CommandResult, + validate_command_kill_scope, +) from e2b.sandbox_sync.commands.command_handle import CommandHandle @@ -91,6 +99,8 @@ def kill( self, pid: int, request_timeout: Optional[float] = None, + *, + scope: CommandKillScope = "process", ) -> bool: """ Kill a running command specified by its process ID. @@ -98,14 +108,23 @@ def kill( :param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()` :param request_timeout: Timeout for the request in **seconds** + :param scope: Whether to kill only the managed process or its process group :return: `True` if the command was killed, `False` if the command was not found """ + validate_command_kill_scope(scope) + if scope == "group" and self._envd_version < ENVD_COMMANDS_DESCENDANTS: + raise SandboxException( + f"Sandbox envd version {self._envd_version} doesn't support group-scoped command termination. " + "Please rebuild your template to pick up the latest sandbox version." + ) + try: self._rpc.send_signal( process_pb.SendSignalRequest( process=process_pb.ProcessSelector(selector=Oneof("pid", pid)), signal=process_pb.Signal.SIGKILL, + descendants=scope == "group", ), timeout_ms=timeout_to_ms( self._connection_config.get_request_timeout(request_timeout) @@ -321,7 +340,9 @@ def _start( pid = extract_start_pid(start_event, "start process") return CommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, handle_send_stdin=lambda data, request_timeout=None: self.send_stdin( pid, data, request_timeout @@ -372,7 +393,9 @@ def connect( pid = extract_start_pid(start_event, "connect to process") return CommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, handle_send_stdin=lambda data, request_timeout=None: self.send_stdin( pid, data, request_timeout diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py index ea2b3fdad7..ee84425363 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py @@ -9,6 +9,7 @@ from e2b.exceptions import SandboxException from e2b.sandbox.commands.command_handle import ( CommandExitException, + CommandKillScope, CommandResult, Stderr, Stdout, @@ -33,7 +34,7 @@ def pid(self): def __init__( self, pid: int, - handle_kill: Callable[[], bool], + handle_kill: Callable[[CommandKillScope, Optional[float]], bool], events: Generator[ Union[process_pb.StartResponse, process_pb.ConnectResponse], Any, None ], @@ -205,15 +206,22 @@ def wait( return self._result - def kill(self) -> bool: + def kill( + self, + *, + scope: CommandKillScope = "process", + request_timeout: Optional[float] = None, + ) -> bool: """ Kills the command. It uses `SIGKILL` signal to kill the command. + :param scope: Whether to kill only the managed process or its process group + :param request_timeout: Timeout for the request in **seconds** :return: Whether the command was killed successfully """ - return self._handle_kill() + return self._handle_kill(scope, request_timeout) def send_stdin( self, diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/pty.py b/packages/python-sdk/e2b/sandbox_sync/commands/pty.py index 9465007759..d320977422 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/pty.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/pty.py @@ -16,14 +16,20 @@ ) from e2b.envd.api import check_sandbox_health from e2b.envd.rpc import handle_rpc_exception_with_health +from e2b.envd.versions import ENVD_COMMANDS_DESCENDANTS from e2b.envd.utils import ( authentication_header, extract_start_pid, timeout_to_ms, ) from e2b.envd.client_sync import as_stream, create_rpc_client -from e2b.sandbox.commands.command_handle import PtySize +from e2b.sandbox.commands.command_handle import ( + CommandKillScope, + PtySize, + validate_command_kill_scope, +) from e2b.sandbox_sync.commands.command_handle import CommandHandle +from e2b.exceptions import SandboxException class Pty: @@ -54,20 +60,31 @@ def kill( self, pid: int, request_timeout: Optional[float] = None, + *, + scope: CommandKillScope = "process", ) -> bool: """ Kill PTY. :param pid: Process ID of the PTY :param request_timeout: Timeout for the request in **seconds** + :param scope: Whether to kill only the managed PTY process or its process group :return: `true` if the PTY was killed, `false` if the PTY was not found """ + validate_command_kill_scope(scope) + if scope == "group" and self._envd_version < ENVD_COMMANDS_DESCENDANTS: + raise SandboxException( + f"Sandbox envd version {self._envd_version} doesn't support group-scoped command termination. " + "Please rebuild your template to pick up the latest sandbox version." + ) + try: self._rpc.send_signal( process_pb.SendSignalRequest( process=process_pb.ProcessSelector(selector=Oneof("pid", pid)), signal=process_pb.Signal.SIGKILL, + descendants=scope == "group", ), timeout_ms=timeout_to_ms( self._connection_config.get_request_timeout(request_timeout) @@ -160,7 +177,9 @@ def create( pid = extract_start_pid(start_event, "start process") return CommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, check_health=self._check_health, ) @@ -204,7 +223,9 @@ def connect( pid = extract_start_pid(start_event, "connect to process") return CommandHandle( pid=pid, - handle_kill=lambda: self.kill(pid), + handle_kill=lambda scope="process", request_timeout=None: self.kill( + pid, request_timeout, scope=scope + ), events=events, check_health=self._check_health, ) diff --git a/packages/python-sdk/tests/test_command_handle.py b/packages/python-sdk/tests/test_command_handle.py index 6a2f81c1ad..ff7c7b569a 100644 --- a/packages/python-sdk/tests/test_command_handle.py +++ b/packages/python-sdk/tests/test_command_handle.py @@ -1,13 +1,20 @@ import asyncio +from types import SimpleNamespace from typing import Any, cast import pytest +from packaging.version import Version from protobuf import Oneof from e2b.envd.process import process_pb +from e2b.exceptions import InvalidArgumentException, SandboxException from e2b.sandbox_async.commands.command_handle import AsyncCommandHandle +from e2b.sandbox_async.commands.command import Commands as AsyncCommands +from e2b.sandbox_async.commands.pty import Pty as AsyncPty from e2b.sandbox_sync.commands.command_handle import CommandHandle +from e2b.sandbox_sync.commands.command import Commands as SyncCommands +from e2b.sandbox_sync.commands.pty import Pty as SyncPty EMOJI = "😀" EMOJI_BYTES = EMOJI.encode("utf-8") # 4 bytes @@ -46,10 +53,194 @@ def _end_event(exit_code: int = 0) -> process_pb.StartResponse: ) -async def _kill() -> bool: +async def _kill(_scope="process", _request_timeout=None) -> bool: return True +def test_sync_kill_forwards_scope_and_timeout(): + options = [] + handle = CommandHandle( + pid=1, + handle_kill=lambda scope, request_timeout: options.append( + (scope, request_timeout) + ) + or True, + events=iter(()), + ) + + assert handle.kill(scope="group", request_timeout=3.0) + assert options == [("group", 3.0)] + + +async def test_async_kill_forwards_scope_and_timeout(): + options = [] + + async def kill(scope, request_timeout) -> bool: + options.append((scope, request_timeout)) + return True + + async def events(): + if False: + yield None + + handle = AsyncCommandHandle(pid=1, handle_kill=kill, events=events()) + + assert await handle.kill(scope="group", request_timeout=3.0) + assert options == [("group", 3.0)] + + +def test_sync_commands_kill_sends_descendant_scope(): + requests = [] + commands = object.__new__(SyncCommands) + commands._rpc = SimpleNamespace( + send_signal=lambda request, **_kwargs: requests.append(request) + ) + commands._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + commands._envd_version = Version("0.7.1") + + assert commands.kill(42, scope="group") + assert requests[0].descendants is True + + +async def test_async_commands_kill_sends_descendant_scope(): + requests = [] + + async def send_signal(request, **_kwargs): + requests.append(request) + + commands = object.__new__(AsyncCommands) + commands._rpc = SimpleNamespace(send_signal=send_signal) + commands._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + commands._envd_version = Version("0.7.1") + + assert await commands.kill(42, scope="group") + assert requests[0].descendants is True + + +def test_sync_pty_kill_sends_group_scope(): + requests = [] + pty = object.__new__(SyncPty) + pty._rpc = SimpleNamespace( + send_signal=lambda request, **_kwargs: requests.append(request) + ) + pty._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + pty._envd_version = Version("0.7.1") + + assert pty.kill(43, scope="group") + assert requests[0].descendants is True + + +async def test_async_pty_kill_sends_group_scope(): + requests = [] + + async def send_signal(request, **_kwargs): + requests.append(request) + + pty = object.__new__(AsyncPty) + pty._rpc = SimpleNamespace(send_signal=send_signal) + pty._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + pty._envd_version = Version("0.7.1") + + assert await pty.kill(43, scope="group") + assert requests[0].descendants is True + + +def test_sync_commands_kill_rejects_invalid_scope_before_rpc(): + requests = [] + commands = object.__new__(SyncCommands) + commands._rpc = SimpleNamespace( + send_signal=lambda request, **_kwargs: requests.append(request) + ) + commands._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + commands._envd_version = Version("0.7.1") + + with pytest.raises(InvalidArgumentException, match="scope must be one of"): + commands.kill(42, scope=cast(Any, "groups")) + assert requests == [] + + +async def test_async_commands_kill_rejects_invalid_scope_before_rpc(): + requests = [] + + async def send_signal(request, **_kwargs): + requests.append(request) + + commands = object.__new__(AsyncCommands) + commands._rpc = SimpleNamespace(send_signal=send_signal) + commands._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + commands._envd_version = Version("0.7.1") + + with pytest.raises(InvalidArgumentException, match="scope must be one of"): + await commands.kill(42, scope=cast(Any, "groups")) + assert requests == [] + + +def test_sync_pty_kill_rejects_invalid_scope_before_rpc(): + requests = [] + pty = object.__new__(SyncPty) + pty._rpc = SimpleNamespace( + send_signal=lambda request, **_kwargs: requests.append(request) + ) + pty._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + pty._envd_version = Version("0.7.1") + + with pytest.raises(InvalidArgumentException, match="scope must be one of"): + pty.kill(43, scope=cast(Any, "groups")) + assert requests == [] + + +async def test_async_pty_kill_rejects_invalid_scope_before_rpc(): + requests = [] + + async def send_signal(request, **_kwargs): + requests.append(request) + + pty = object.__new__(AsyncPty) + pty._rpc = SimpleNamespace(send_signal=send_signal) + pty._connection_config = SimpleNamespace( + get_request_timeout=lambda request_timeout: request_timeout + ) + pty._envd_version = Version("0.7.1") + + with pytest.raises(InvalidArgumentException, match="scope must be one of"): + await pty.kill(43, scope=cast(Any, "groups")) + assert requests == [] + + +def test_sync_commands_kill_rejects_descendant_scope_for_old_envd(): + commands = object.__new__(SyncCommands) + commands._envd_version = Version("0.7.0") + + with pytest.raises( + SandboxException, match="doesn't support group-scoped command termination" + ): + commands.kill(42, scope="group") + + +async def test_async_commands_kill_rejects_descendant_scope_for_old_envd(): + commands = object.__new__(AsyncCommands) + commands._envd_version = Version("0.7.0") + + with pytest.raises( + SandboxException, match="doesn't support group-scoped command termination" + ): + await commands.kill(42, scope="group") + + class _AsyncControllableEvents: """Async event source that delivers items on demand. @@ -127,7 +318,9 @@ def events(): yield _stdout_event(b"a") yield _end_event() - handle = CommandHandle(pid=1, handle_kill=lambda: True, events=events()) + handle = CommandHandle( + pid=1, handle_kill=lambda _scope="process", _timeout=None: True, events=events() + ) # Nothing is consumed until the caller iterates. assert consumed == [] @@ -145,7 +338,9 @@ def events(): yield _stdout_event(b"a" + EMOJI_BYTES[:2]) yield _end_event(0) - handle = CommandHandle(pid=1, handle_kill=lambda: True, events=events()) + handle = CommandHandle( + pid=1, handle_kill=lambda _scope="process", _timeout=None: True, events=events() + ) iterator = iter(handle) assert next(iterator) == ("a", None, None) # The end event flushes a trailing replacement character; pull just that @@ -167,7 +362,9 @@ def events(): yield _end_event() chunks = [] - handle = CommandHandle(pid=1, handle_kill=lambda: True, events=events()) + handle = CommandHandle( + pid=1, handle_kill=lambda _scope="process", _timeout=None: True, events=events() + ) result = handle.wait(on_stdout=chunks.append) assert result.stdout == f"a{EMOJI}b" @@ -182,7 +379,9 @@ def events(): yield _stdout_event(b"a" + EMOJI_BYTES[:2]) yield _end_event() - handle = CommandHandle(pid=1, handle_kill=lambda: True, events=events()) + handle = CommandHandle( + pid=1, handle_kill=lambda _scope="process", _timeout=None: True, events=events() + ) result = handle.wait() assert result.stdout == "a�" @@ -228,7 +427,9 @@ def events(): yield _stdout_event(b"a" + EMOJI_BYTES[:2]) chunks = [] - handle = CommandHandle(pid=1, handle_kill=lambda: True, events=events()) + handle = CommandHandle( + pid=1, handle_kill=lambda _scope="process", _timeout=None: True, events=events() + ) for stdout, _, _ in handle: if stdout is not None: chunks.append(stdout) @@ -258,7 +459,9 @@ def events(): raise RuntimeError("stream died") chunks = [] - handle = CommandHandle(pid=1, handle_kill=lambda: True, events=events()) + handle = CommandHandle( + pid=1, handle_kill=lambda _scope="process", _timeout=None: True, events=events() + ) # The stream raises before an end event, but the buffered bytes must still # be flushed as a replacement character before the error is surfaced. diff --git a/spec/envd/envd.yaml b/spec/envd/envd.yaml index a6c0500090..d385e6d587 100644 --- a/spec/envd/envd.yaml +++ b/spec/envd/envd.yaml @@ -96,14 +96,43 @@ paths: what this call caused. Best-effort by design: a workload that will not quiesce within the budget is - reported as unconfirmed rather than failing the call, and cgroups that reject the - write are counted in failed, because neither an unfreezable customer task nor a - cgroup disappearing mid-sweep may block their pause. + reported as unconfirmed rather than failing the call, cgroups that reject the write + are counted in failed, and cgroups the guest removed while the sweep was working on + them are counted in vanished. None of the three may block a pause, which is why all + three are counts in the body rather than an error. Whether this endpoint waits is the caller's choice, expressed by supplying maxWaitMs: see that parameter. x-internal: true parameters: + - name: mode + in: query + required: false + description: | + Which cgroups to freeze. "hierarchy" freezes the complement of envd's own + ancestor chain, so cgroups the customer created anywhere in the tree are + covered; "legacy" freezes only the user and pty cgroups envd itself creates. + Omitted means legacy, which is what an orchestrator predating this parameter + gets. + + The mode is chosen by the caller because the feature flag that selects it is + evaluated there — envd has no access to it. FreezeResult echoes the mode back + so the caller can confirm envd honoured the request rather than inferring it + from the flag's value: an envd too old to know about modes reports legacy + while the flag reads on. + schema: + type: string + enum: [legacy, hierarchy] + - name: maxCgroups + in: query + required: false + description: | + Bounds how many cgroups a hierarchy sweep may visit. A safety guard against a + pathological or hostile hierarchy rather than a performance knob — the guest is + the threat model. Omitted or non-positive means envd's own default. Ignored in + legacy mode. + schema: + type: integer - name: maxWaitMs in: query required: false @@ -522,6 +551,22 @@ components: type: object description: Per-call statistics from a pre-pause workload freeze properties: + mode: + type: string + enum: [legacy, hierarchy] + description: Which sweep actually ran. Echoed back rather than inferred from the flag, so a caller can tell that envd honoured what it asked for + visited: + type: integer + description: Cgroups the walk examined, whether or not it froze them. The input for sizing the bound; meaningless in legacy mode + allowlisted: + type: integer + description: Cgroups skipped because the resume path depends on them (systemd, journald, envd's port forwarding). Reported because the allowlist is expected to grow, and a distro that routes journald differently changes this count + truncated: + type: boolean + description: True when the walk stopped because it hit the bound rather than because it ran out of tree, so coverage is incomplete + preFrozen: + type: integer + description: Cgroups the guest itself had already frozen before the sweep ran (docker pause writes cgroup.freeze). Not written to and deliberately left frozen by the resume thaw, so the guest's own suspension survives the snapshot requested: type: integer description: Cgroups this call wrote cgroup.freeze to @@ -533,7 +578,10 @@ components: description: Cgroups still reading "frozen 0" when the budget expired; their tasks may still be running, so a snapshot taken now can capture a live workload failed: type: integer - description: Cgroups whose freeze write or state read errored (expected for a threaded cgroup, and for one removed mid-sweep) + description: Cgroups that errored for a reason that is not simply being gone - the write was refused, or the state could not be read. A cgroup the hierarchy walk discovered that merely went away is counted vanished instead; one of envd's own static cgroups that goes away is counted here, by design + vanished: + type: integer + description: Cgroups the hierarchy walk enumerated that the guest then removed before the sweep finished with them - envd's own static cgroups are excluded and report failed instead. A race rather than a failure, and a claim about the cgroup only - tasks migrated out of it before its removal can still be running. Like failed it spans both phases, so it does not reconcile against requested on its own - one removed during the settle poll was counted in requested, one removed before its write never was unobservable: type: integer description: Cgroups whose freeze state cannot be read because this guest has no cgroup manager; the write was accepted but nothing can be read back, so these are neither frozen nor notFrozen diff --git a/spec/envd/process/process.proto b/spec/envd/process/process.proto index 99376a0e37..d9029d0d44 100644 --- a/spec/envd/process/process.proto +++ b/spec/envd/process/process.proto @@ -149,6 +149,10 @@ message SendSignalRequest { ProcessSelector process = 1; Signal signal = 2; + + // Signal the command's process group instead of only its leader. + // This includes descendants that remain in the command's process group. + bool descendants = 3; } message SendSignalResponse {} diff --git a/spec/infra-ref b/spec/infra-ref index 40c51438b1..b7356035bc 100644 --- a/spec/infra-ref +++ b/spec/infra-ref @@ -1 +1 @@ -e19a12b8fc5d318c6e88a8edba0a94d1f153a841 +c5a0bdbaf7f84956945fb144e2194df1d6c385fe diff --git a/spec/openapi.yml b/spec/openapi.yml index 6ddde58ad7..4ccdbf3f9e 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -12,14 +12,6 @@ components: type: apiKey in: header name: X-API-Key - AccessTokenAuth: - type: http - scheme: bearer - bearerFormat: access_token - description: | - **Deprecated.** Access token authentication is deprecated and will be - removed in a future release. Use API key authentication (`X-API-Key`) - instead. # AuthProviderBearerAuth / AuthProviderTeamAuth: B before T in the name # so Bearer is validated before Team. AuthProviderBearerAuth: @@ -36,12 +28,31 @@ components: type: apiKey in: header name: X-Admin-Token + AdminJWTAuth: + type: http + scheme: bearer + bearerFormat: JWT AdminTeamAuth: type: apiKey in: header name: X-Team-ID parameters: + clusterID: + name: clusterID + in: path + required: true + schema: + type: string + format: uuid + description: Identifier of the cluster + rigID: + name: rigID + in: path + required: true + schema: + type: string + description: Rig identifier (e.g. "default") templateID: name: templateID in: path @@ -78,12 +89,6 @@ components: required: true schema: type: string - accessTokenID: - name: accessTokenID - in: path - required: true - schema: - type: string snapshotID: name: snapshotID in: path @@ -176,12 +181,6 @@ components: application/json: schema: $ref: "#/components/schemas/Error" - "410": - description: Gone - content: - application/json: - schema: - $ref: "#/components/schemas/Error" "429": description: Too many requests content: @@ -194,6 +193,12 @@ components: application/json: schema: $ref: "#/components/schemas/Error" + "501": + description: Not implemented by this deployment + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "502": description: Backend error content: @@ -214,6 +219,103 @@ components: $ref: "#/components/schemas/Error" schemas: + Rig: + description: An orchestrator node pool backed by one cloud scaling group + required: + - id + - provider + - resourceID + - capacityDesired + - capacityCurrent + properties: + id: + type: string + description: Rig identifier (e.g. "default") + provider: + type: string + description: Cloud provider backing the rig ("aws" or "gcp") + resourceID: + type: string + description: Canonical cloud resource ID of the scaling group backing the rig (ARN on AWS, self-link on GCP) + capacityDesired: + type: integer + format: int32 + description: Desired number of instances in the rig + capacityMin: + type: integer + format: int32 + description: > + Minimum capacity enforced on the rig's scaling group. Omitted when + nothing enforces bounds (GCP MIG without an active autoscaler). + capacityMax: + type: integer + format: int32 + description: > + Maximum capacity enforced on the rig's scaling group. Omitted when + nothing enforces bounds (GCP MIG without an active autoscaler). + capacityCurrent: + type: integer + format: int32 + description: Number of instances currently attached to the rig + + RigCapacityChange: + description: Desired capacity to set on the rig's scaling group + required: + - desired + properties: + desired: + type: integer + format: int32 + minimum: 0 + description: Absolute desired number of instances in the rig + + RigInstance: + description: An instance attached to a rig's scaling group + required: + - id + - transitioning + - terminating + properties: + id: + type: string + description: Provider instance ID (EC2 instance ID on AWS, instance name on GCP), also the node ID the orchestrator reports + createdAt: + type: string + format: date-time + description: When the provider created the instance. Omitted while the instance is transitioning. + transitioning: + type: boolean + description: The provider is creating, deleting, recreating or otherwise mutating the instance + terminating: + type: boolean + description: The instance is on its way out of the group and can never become healthy again + + RigError: + description: > + Scaling error on the rig's scaling group, e.g. a failed instance + creation due to resource exhaustion + required: + - timestamp + - code + - message + properties: + timestamp: + type: string + format: date-time + description: When the error occurred + code: + type: string + description: Provider-specific error code (e.g. ZONE_RESOURCE_POOL_EXHAUSTED, Failed) + message: + type: string + description: Human-readable error message + instance: + type: string + description: Instance the error relates to, if any + action: + type: string + description: Action being performed when the error occurred (e.g. CREATING) + Team: required: - teamID @@ -356,12 +458,27 @@ components: maskRequestHost: type: string description: Specify host mask which will be used for all sandbox requests + httpsPorts: + type: array + description: Sandbox ports that serve HTTPS rather than plaintext HTTP. Affects how the proxy reaches the service inside the sandbox; the public URL is HTTPS either way. Certificates are not verified, so self-signed ones work. The envd port (49983) cannot be listed. + maxItems: 128 + uniqueItems: true + items: + type: integer + format: uint32 + minimum: 1 + maximum: 65535 rules: type: object description: > - Per-domain transform rules applied to matching egress HTTP/HTTPS requests. - Keys are domains (e.g. "api.example.com", "example.com"). - A domain listed here is not automatically allowed - use allowOut to permit the traffic. + Per-domain transform rules applied to matching outbound HTTPS requests. + Keys may be exact DNS names (for example, "api.example.com") or a leading wildcard + (for example, "*.example.com"), and are normalized to lowercase on write. + Wildcards match subdomains at any depth but not the apex domain; a bare "*" is invalid. + Exact rules take precedence, followed by the longest matching wildcard suffix, and + matching rule sets are not merged. Broad wildcards such as "*.com" are allowed and may + expose transformed credentials to every matching destination the sandbox contacts. + Rules do not grant network access; configure allowOut separately to permit the destination. additionalProperties: type: array items: @@ -385,7 +502,15 @@ components: $ref: "#/components/schemas/SandboxEgressProxyConfig" rules: type: object - description: Per-domain transform rules. Replaces all existing rules when provided. + description: > + Per-domain transform rules applied to matching outbound HTTPS requests. Replaces all + existing rules when provided. Keys may be exact DNS names or a single leading wildcard + (for example, "*.example.com"), and are normalized to lowercase on write. Wildcards match + subdomains at any depth but not the apex domain; a bare "*" is invalid. Exact rules take + precedence, followed by the longest matching wildcard suffix, and matching rule sets are + not merged. Broad wildcards such as "*.com" are allowed and may expose transformed + credentials to every matching destination the sandbox contacts. Rules do not grant + network access; configure allowOut separately to permit the destination. additionalProperties: type: array items: @@ -854,6 +979,16 @@ components: type: boolean deprecated: true description: Automatically pauses the sandbox after the timeout + memory: + type: boolean + x-not-implemented: true + description: >- + Defaults to true. When false, resume from disk state only: the sandbox cold-boots fresh and + any memory in the snapshot is ignored, never modified or deleted. Disk + state has crash-recovery semantics — writes not flushed before the pause + may be lost. A no-op for snapshots that contain no memory. Rejected with + an error in environments where this capability is not enabled, never + silently downgraded to a memory restore. ConnectSandbox: type: object @@ -865,6 +1000,16 @@ components: type: integer format: int32 minimum: 0 + memory: + type: boolean + x-not-implemented: true + description: >- + Defaults to true. When false and the sandbox is paused, resume from disk state only: the + sandbox cold-boots fresh and any memory in the snapshot is ignored, never + modified or deleted. Disk state has crash-recovery semantics — writes not + flushed before the pause may be lost. A no-op for snapshots that contain + no memory. Rejected with an error in environments where this capability + is not enabled, never silently downgraded to a memory restore. SandboxTimeoutRequest: type: object @@ -1859,39 +2004,6 @@ components: format: uint64 description: Number of sandbox create fails - CreatedAccessToken: - required: - - id - - name - - token - - mask - - createdAt - properties: - id: - type: string - format: uuid - description: Identifier of the access token - name: - type: string - description: Name of the access token - token: - type: string - description: The fully created access token - mask: - $ref: "#/components/schemas/IdentifierMaskingDetails" - createdAt: - type: string - format: date-time - description: Timestamp of access token creation - - NewAccessToken: - required: - - name - properties: - name: - type: string - description: Name of the access token - TeamAPIKey: required: - id @@ -2198,7 +2310,6 @@ tags: - name: templates - name: sandboxes - name: auth - - name: access-tokens - name: api-keys - name: tags - name: volumes @@ -2221,7 +2332,6 @@ paths: description: List all teams tags: [auth] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] responses: "200": @@ -2248,6 +2358,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/teamID" - in: query @@ -2293,6 +2405,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/teamID" - in: query @@ -2344,6 +2458,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - name: metadata in: query @@ -2376,6 +2492,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -2411,6 +2529,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - name: metadata in: query @@ -2481,6 +2601,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - name: sandbox_ids in: query @@ -2519,6 +2641,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" - in: query @@ -2561,6 +2685,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" - in: query @@ -2620,6 +2746,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" responses: @@ -2646,6 +2774,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" responses: @@ -2669,6 +2799,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" - in: query @@ -2716,6 +2848,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" requestBody: @@ -2748,10 +2882,12 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" requestBody: - required: true + required: false content: application/json: schema: @@ -2796,6 +2932,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" requestBody: @@ -2837,6 +2975,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" requestBody: @@ -2883,6 +3023,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] tags: [sandboxes] requestBody: content: @@ -2911,6 +3053,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] tags: [sandboxes] requestBody: required: true @@ -2942,6 +3086,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] tags: [sandboxes] requestBody: content: @@ -2969,6 +3115,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/sandboxID" requestBody: @@ -3004,6 +3152,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - name: sandboxID in: query @@ -3047,6 +3197,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -3077,11 +3229,12 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - in: query required: false @@ -3122,6 +3275,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -3149,12 +3304,13 @@ paths: description: Get an upload link for a tar file containing build layer files tags: [templates] security: - - AccessTokenAuth: [] - ApiKeyAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" - in: path @@ -3188,11 +3344,12 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - in: query required: false @@ -3219,7 +3376,6 @@ paths: deprecated: true tags: [templates] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] requestBody: @@ -3254,6 +3410,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" - $ref: "#/components/parameters/paginationNextToken" @@ -3278,7 +3436,6 @@ paths: deprecated: true tags: [templates] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] parameters: @@ -3307,11 +3464,12 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" responses: @@ -3328,11 +3486,12 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" requestBody: @@ -3358,7 +3517,6 @@ paths: deprecated: true tags: [templates] security: - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] parameters: @@ -3383,6 +3541,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" - $ref: "#/components/parameters/buildID" @@ -3407,11 +3567,12 @@ paths: tags: [templates] security: - ApiKeyAuth: [] - - AccessTokenAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" requestBody: @@ -3440,12 +3601,13 @@ paths: description: Get template build info tags: [templates] security: - - AccessTokenAuth: [] - ApiKeyAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" - $ref: "#/components/parameters/buildID" @@ -3490,12 +3652,13 @@ paths: description: Get template build logs tags: [templates] security: - - AccessTokenAuth: [] - ApiKeyAuth: [] - AuthProviderBearerAuth: [] AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" - $ref: "#/components/parameters/buildID" @@ -3553,6 +3716,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -3584,6 +3749,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -3613,6 +3780,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/templateID" responses: @@ -3644,6 +3813,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - name: alias in: path @@ -3674,6 +3845,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - in: query name: clusterID @@ -3703,6 +3875,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - $ref: "#/components/parameters/nodeID" - in: query @@ -3731,6 +3904,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - $ref: "#/components/parameters/nodeID" requestBody: @@ -3757,6 +3931,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - name: teamID in: path @@ -3788,6 +3963,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] responses: "200": description: Running sandbox counts keyed by team ID @@ -3807,6 +3983,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - name: teamID in: path @@ -3836,6 +4013,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - name: teamID in: path @@ -3875,6 +4053,7 @@ paths: tags: [admin] security: - AdminApiKeyAuth: [] + - AdminJWTAuth: [] parameters: - name: teamID in: path @@ -3896,53 +4075,6 @@ paths: "500": $ref: "#/components/responses/500" - /access-tokens: - post: - summary: Create access token - description: Create a new access token. Deprecated; use an API key (E2B_API_KEY) instead. - deprecated: true - tags: [access-tokens] - security: - - AuthProviderBearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/NewAccessToken" - responses: - "201": - description: Access token created successfully - content: - application/json: - schema: - $ref: "#/components/schemas/CreatedAccessToken" - "401": - $ref: "#/components/responses/401" - "410": - $ref: "#/components/responses/410" - "500": - $ref: "#/components/responses/500" - - /access-tokens/{accessTokenID}: - delete: - summary: Delete access token - description: Delete an access token - tags: [access-tokens] - security: - - AuthProviderBearerAuth: [] - parameters: - - $ref: "#/components/parameters/accessTokenID" - responses: - "204": - description: Access token deleted successfully - "401": - $ref: "#/components/responses/401" - "404": - $ref: "#/components/responses/404" - "500": - $ref: "#/components/responses/500" - /api-keys: get: summary: List team API keys @@ -3953,6 +4085,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] responses: "200": description: Successfully returned all team API keys @@ -4001,6 +4135,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/apiKeyID" requestBody: @@ -4027,6 +4163,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/apiKeyID" responses: @@ -4050,6 +4188,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] responses: "200": description: Successfully listed all team volumes @@ -4074,6 +4214,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -4105,6 +4247,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/volumeID" responses: @@ -4131,6 +4275,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/volumeID" responses: @@ -4154,6 +4300,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/paginationNextToken" - $ref: "#/components/parameters/paginationLimit" @@ -4198,6 +4346,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] requestBody: required: true content: @@ -4241,6 +4391,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/secretID" responses: @@ -4279,6 +4431,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/secretID" requestBody: @@ -4323,6 +4477,8 @@ paths: AuthProviderTeamAuth: [] - AdminApiKeyAuth: [] AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] parameters: - $ref: "#/components/parameters/secretID" responses: @@ -4346,3 +4502,189 @@ paths: $ref: "#/components/responses/502" "504": $ref: "#/components/responses/504" + + /clusters/{clusterID}/rigs: + get: + summary: List rigs of a cluster + description: > + List the orchestrator node pools ("rigs") of a cluster with a snapshot + of their scaling groups. Forwarded to the cluster's edge service; a + cluster with no rig management configured returns an empty list, and + the local cluster answers 501. + tags: [admin] + security: + - AdminApiKeyAuth: [] + - AdminJWTAuth: [] + parameters: + - $ref: "#/components/parameters/clusterID" + responses: + "200": + description: Successfully returned the rigs of the cluster + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Rig" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + "501": + $ref: "#/components/responses/501" + + /clusters/{clusterID}/rigs/{rigID}/capacity: + put: + summary: Set the capacity of a rig + description: > + Set the desired instance count on the rig's scaling group. The value is + passed to the cloud provider unchanged; violations of the group's bounds + or conflicting concurrent operations surface as errors. + tags: [admin] + security: + - AdminApiKeyAuth: [] + - AdminJWTAuth: [] + parameters: + - $ref: "#/components/parameters/clusterID" + - $ref: "#/components/parameters/rigID" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RigCapacityChange" + responses: + "202": + description: Capacity change accepted + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "500": + $ref: "#/components/responses/500" + "501": + $ref: "#/components/responses/501" + + /clusters/{clusterID}/rigs/instances/{instanceID}: + delete: + summary: Terminate an instance of a rig + description: > + Terminate an instance in whichever rig's scaling group it belongs to. + The caller chooses whether the rig shrinks or the instance is replaced. + tags: [admin] + security: + - AdminApiKeyAuth: [] + - AdminJWTAuth: [] + parameters: + - $ref: "#/components/parameters/clusterID" + - name: instanceID + in: path + required: true + schema: + type: string + description: Provider instance ID + - name: decrementDesired + in: query + required: true + schema: + type: boolean + description: > + When true, desired capacity is decremented (rig shrinks); + when false, the scaling group launches a replacement instance + responses: + "202": + description: Instance termination accepted + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "500": + $ref: "#/components/responses/500" + "501": + $ref: "#/components/responses/501" + + /clusters/{clusterID}/rigs/{rigID}/instances: + get: + summary: List the instances attached to a rig + description: > + List the instances attached to the rig's scaling group with their + creation time and transition state, sorted by instance ID. + tags: [admin] + security: + - AdminApiKeyAuth: [] + - AdminJWTAuth: [] + parameters: + - $ref: "#/components/parameters/clusterID" + - $ref: "#/components/parameters/rigID" + responses: + "200": + description: Successfully returned the instances of the rig + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RigInstance" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + "501": + $ref: "#/components/responses/501" + + /clusters/{clusterID}/rigs/{rigID}/errors: + get: + summary: List recent scaling errors of a rig + description: > + List recent scaling errors on the rig's scaling group (e.g. failed + instance creations due to resource exhaustion), newest first. + tags: [admin] + security: + - AdminApiKeyAuth: [] + - AdminJWTAuth: [] + parameters: + - $ref: "#/components/parameters/clusterID" + - $ref: "#/components/parameters/rigID" + - name: limit + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 50 + default: 20 + description: Maximum number of errors to return + responses: + "200": + description: Successfully returned the scaling errors of the rig + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RigError" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "500": + $ref: "#/components/responses/500" + "501": + $ref: "#/components/responses/501"