From 93a5d8851406915e1c6bdd91272be74b96b14844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:22:51 +0000 Subject: [PATCH] build(deps): bump github.com/digitalocean/godo from 1.208.0 to 1.215.0 Bumps [github.com/digitalocean/godo](https://github.com/digitalocean/godo) from 1.208.0 to 1.215.0. - [Release notes](https://github.com/digitalocean/godo/releases) - [Changelog](https://github.com/digitalocean/godo/blob/main/CHANGELOG.md) - [Commits](https://github.com/digitalocean/godo/compare/v1.208.0...v1.215.0) --- updated-dependencies: - dependency-name: github.com/digitalocean/godo dependency-version: 1.215.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +- .../github.com/digitalocean/godo/CHANGELOG.md | 30 + .../digitalocean/godo/CONTRIBUTING.md | 11 + vendor/github.com/digitalocean/godo/godo.go | 31 +- .../godo/hosted_agent_checkpoints.go | 224 +++ .../digitalocean/godo/hosted_agent_configs.go | 228 +++ .../digitalocean/godo/hosted_agent_policy.go | 95 + .../godo/hosted_agent_templates.go | 310 +++ .../godo/hosted_agent_triggers.go | 536 +++++ .../digitalocean/godo/hosted_agents.go | 1778 +++++++++++++++++ .../digitalocean/godo/kubernetes.go | 8 + .../digitalocean/godo/micro_droplets.go | 551 ----- .../github.com/digitalocean/godo/microvms.go | 744 +++++++ vendor/github.com/digitalocean/godo/nfs.go | 1 + .../digitalocean/godo/prepayment.go | 96 + .../github.com/digitalocean/godo/streaming.go | 14 + vendor/modules.txt | 2 +- 18 files changed, 4105 insertions(+), 560 deletions(-) create mode 100644 vendor/github.com/digitalocean/godo/hosted_agent_checkpoints.go create mode 100644 vendor/github.com/digitalocean/godo/hosted_agent_configs.go create mode 100644 vendor/github.com/digitalocean/godo/hosted_agent_policy.go create mode 100644 vendor/github.com/digitalocean/godo/hosted_agent_templates.go create mode 100644 vendor/github.com/digitalocean/godo/hosted_agent_triggers.go create mode 100644 vendor/github.com/digitalocean/godo/hosted_agents.go delete mode 100644 vendor/github.com/digitalocean/godo/micro_droplets.go create mode 100644 vendor/github.com/digitalocean/godo/microvms.go create mode 100644 vendor/github.com/digitalocean/godo/prepayment.go diff --git a/go.mod b/go.mod index ff9b5259e..cd45551dd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.27.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/digitalocean/godo v1.208.0 + github.com/digitalocean/godo v1.215.0 github.com/go-logr/logr v1.4.4 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index baafc8f0a..24e3f787b 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/digitalocean/godo v1.208.0 h1:J1+thyQyuU5OXRvifI3NKQZNirNiY6jzEPbvqOQ8yoI= -github.com/digitalocean/godo v1.208.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/digitalocean/godo v1.215.0 h1:lRosjApOwmQnnkjHnIRV81PGxQehZI7RcIU9QP6r6Yw= +github.com/digitalocean/godo v1.215.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index 66f4b96d1..875ab5d47 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,5 +1,35 @@ # Change Log +## [1.215.0] - 2026-09-24 + +- #1127 - @vrindavinod-do - hosted_agents: add ZeroBalance pause reason, deprecate LowBalance + +## [1.214.0] - 2026-09-24 + +- #1125 - @SSharma-10 - Add Environment aliases for Managed Agents +- #1123 - @SSharma-10 - Add post-release checksum verification steps to CONTRIBUTING.md + +## [1.213.0] - 2026-09-23 + +- #1102 - @kgautam676 - Adding nfs_csi_plugin to kubernetes cluster types +- #1120 - @sgupta832 - microvm: decode http_protocol, checkpoint size, and combined list filters + +## [1.212.0] - 2026-09-22 + +- #1117 - @sgupta832 - MicroVM URN flip + Exec/ConsoleURL (MDROP-392, MDROP-427) + +## [1.211.0] - 2026-09-22 + +- #1021 - @SSharma-10 - Add OHS endpoints + +## [1.210.0] - 2026-09-18 + +- #1110 - @kwadhwa-source - Mdrop 391/create options sizes + +## [1.209.0] - 2026-09-17 + +- #1107 - @rodrigoea - billing(prepayment): add public config and status client methods + ## [1.208.0] - 2026-09-15 - #1105 - @AKatruwar - registry: expose registry UUID and add ListByUUID diff --git a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md index 4eb32251c..f17e6c772 100644 --- a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md +++ b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md @@ -59,6 +59,17 @@ number for the tag. 5. Once the release process completes, review the draft release for correctness and publish the release. Ensure the release has been marked `Latest`. +6. Verify the module checksum against the Go checksum database (to catch retagging issues): + + 1. Run `go mod tidy` inside doctl (or any other repo that uses godo) so that the corresponding godo hash in the `go.sum` file gets updated. + 2. Hit the curl command in the terminal for the recent release version, for example: + + ```bash + curl https://sum.golang.org/lookup/github.com/digitalocean/godo@v1.165.1 + ``` + + 3. Both hashes should match for that specific godo version. + ## Go Version Support This project follows the support [policy of Go](https://go.dev/doc/devel/release#policy) diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index 3e57c8f10..e19e1dee4 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -21,7 +21,7 @@ import ( ) const ( - libraryVersion = "1.208.0" + libraryVersion = "1.215.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -59,6 +59,7 @@ type Client struct { Apps AppsService Balance BalanceService BillingHistory BillingHistoryService + Prepayment PrepaymentService CDNs CDNService Certificates CertificatesService Databases DatabasesService @@ -77,7 +78,7 @@ type Client struct { Keys KeysService Kubernetes KubernetesService LoadBalancers LoadBalancersService - MicroDroplets MicroDropletsService + MicroVMs MicroVMsService Monitoring MonitoringService Security SecurityService Secrets SecretsService @@ -104,6 +105,8 @@ type Client struct { Routes RoutesService PartnerAttachment PartnerAttachmentService GradientAI GradientAIService + HostedAgents HostedAgentsService + HostedAgentTriggers HostedAgentTriggersService DedicatedInference DedicatedInferenceService BatchInference BatchInferenceService BYOIPPrefixes BYOIPPrefixesService @@ -221,6 +224,16 @@ type ErrorResponse struct { // Attempts is the number of times the request was attempted when retries are enabled. Attempts int + + // NestedError captures the alternate {"error":{"code":...,"message":...}} + // envelope returned by some DigitalOcean services (e.g. the hosted-agents + // harness-api) instead of the top-level {"message":...} shape. When the + // top-level Message is empty, Error() falls back to this message so callers + // see the server's reason rather than a bare status code. + NestedError *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` } // Rate contains the rate limit for the current client. @@ -304,6 +317,7 @@ func NewClient(httpClient *http.Client) *Client { c.Apps = &AppsServiceOp{client: c} c.Balance = &BalanceServiceOp{client: c} c.BillingHistory = &BillingHistoryServiceOp{client: c} + c.Prepayment = &PrepaymentServiceOp{client: c} c.CDNs = &CDNServiceOp{client: c} c.Certificates = &CertificatesServiceOp{client: c} c.Databases = &DatabasesServiceOp{client: c} @@ -321,7 +335,7 @@ func NewClient(httpClient *http.Client) *Client { c.Keys = &KeysServiceOp{client: c} c.Kubernetes = &KubernetesServiceOp{client: c} c.LoadBalancers = &LoadBalancersServiceOp{client: c} - c.MicroDroplets = &MicroDropletsServiceOp{client: c} + c.MicroVMs = &MicroVMsServiceOp{client: c} c.Monitoring = &MonitoringServiceOp{client: c} c.Security = &SecurityServiceOp{client: c} c.Secrets = &SecretsServiceOp{client: c} @@ -350,6 +364,8 @@ func NewClient(httpClient *http.Client) *Client { c.Routes = &RoutesServiceOp{client: c} c.PartnerAttachment = &PartnerAttachmentServiceOp{client: c} c.GradientAI = &GradientAIServiceOp{client: c} + c.HostedAgents = &HostedAgentsServiceOp{client: c} + c.HostedAgentTriggers = &HostedAgentTriggersServiceOp{client: c} c.DedicatedInference = &DedicatedInferenceServiceOp{client: c} batchInferenceURL, _ := url.Parse(defaultBatchInferenceBaseURL) c.BatchInference = &BatchInferenceServiceOp{client: c, baseURL: batchInferenceURL} @@ -684,12 +700,17 @@ func (r *ErrorResponse) Error() string { attempted = fmt.Sprintf("; giving up after %d attempt(s)", r.Attempts) } + message := r.Message + if message == "" && r.NestedError != nil { + message = r.NestedError.Message + } + if r.RequestID != "" { return fmt.Sprintf("%v %v: %d (request %q) %v%s", - r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message, attempted) + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, message, attempted) } return fmt.Sprintf("%v %v: %d %v%s", - r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message, attempted) + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, message, attempted) } // CheckResponse checks the API response for errors, and returns them if present. A response is considered an diff --git a/vendor/github.com/digitalocean/godo/hosted_agent_checkpoints.go b/vendor/github.com/digitalocean/godo/hosted_agent_checkpoints.go new file mode 100644 index 000000000..bb53c3f34 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/hosted_agent_checkpoints.go @@ -0,0 +1,224 @@ +package godo + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +// HostedAgentCheckpointStatus is the lifecycle status of a session checkpoint. +type HostedAgentCheckpointStatus string + +const ( + HostedAgentCheckpointStatusPending HostedAgentCheckpointStatus = "PENDING" + HostedAgentCheckpointStatusReady HostedAgentCheckpointStatus = "READY" + HostedAgentCheckpointStatusFailed HostedAgentCheckpointStatus = "FAILED" +) + +// HostedAgentCheckpointKind distinguishes user-requested vs automatic checkpoints. +type HostedAgentCheckpointKind string + +const ( + HostedAgentCheckpointKindExplicit HostedAgentCheckpointKind = "explicit" + HostedAgentCheckpointKindImplicit HostedAgentCheckpointKind = "implicit" +) + +// HostedAgentCheckpoint is an immutable save point of a session's microVM +// (files + live process memory) plus an event-log cursor. +type HostedAgentCheckpoint struct { + CheckpointID string `json:"checkpoint_id"` + SessionID string `json:"session_id"` + Status HostedAgentCheckpointStatus `json:"status"` + Kind HostedAgentCheckpointKind `json:"kind"` + Label string `json:"label,omitempty"` + EventID string `json:"event_id,omitempty"` + SizeBytes uint64 `json:"size_bytes,omitempty"` + CreatedAt Timestamp `json:"created_at"` + ErrorMessage string `json:"error_message,omitempty"` +} + +// HostedAgentCheckpointCreateRequest is the optional body for CreateCheckpoint. +type HostedAgentCheckpointCreateRequest struct { + Label string `json:"label,omitempty"` +} + +// HostedAgentCheckpointListOptions paginates ListCheckpoints (newest-first). +type HostedAgentCheckpointListOptions struct { + PageToken string `url:"page_token,omitempty"` + PageSize int `url:"page_size,omitempty"` +} + +// HostedAgentCheckpointsListResponse is returned by ListCheckpoints. +type HostedAgentCheckpointsListResponse struct { + Checkpoints []HostedAgentCheckpoint `json:"checkpoints"` + NextPageToken string `json:"next_page_token"` +} + +// HostedAgentCheckpointDeleteResponse is returned by DeleteCheckpoint. +type HostedAgentCheckpointDeleteResponse struct { + CheckpointID string `json:"checkpoint_id"` + Deleted bool `json:"deleted"` +} + +// HostedAgentForkSessionRequest is the body for ForkSession. +// Count defaults to 1 when zero; the server rejects values above HostedAgentForkMaxCount. +type HostedAgentForkSessionRequest struct { + FromCheckpointID string `json:"from_checkpoint_id,omitempty"` + Count int `json:"count,omitempty"` +} + +// HostedAgentForkSessionResponse is returned by ForkSession. +type HostedAgentForkSessionResponse struct { + Sessions []HostedAgentSession `json:"sessions"` +} + +type hostedAgentCheckpointRoot struct { + Checkpoint *HostedAgentCheckpoint `json:"checkpoint"` +} + +// CreateCheckpoint captures a save point for the session. The call blocks until +// the checkpoint reaches a terminal READY state (or the server returns an error). +// Checkpoints are only allowed between turns (at run.completed). +func (s *HostedAgentsServiceOp) CreateCheckpoint(ctx context.Context, sessionID string, create *HostedAgentCheckpointCreateRequest) (*HostedAgentCheckpoint, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if create == nil { + create = &HostedAgentCheckpointCreateRequest{} + } + path := fmt.Sprintf(hostedAgentSessionCheckpointsPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, create) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentCheckpointRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Checkpoint == nil { + return nil, resp, errors.New("hosted agents: create checkpoint returned no checkpoint") + } + return root.Checkpoint, resp, nil +} + +// ListCheckpoints returns checkpoints for a session, newest first. +func (s *HostedAgentsServiceOp) ListCheckpoints(ctx context.Context, sessionID string, opt *HostedAgentCheckpointListOptions) (*HostedAgentCheckpointsListResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + path := fmt.Sprintf(hostedAgentSessionCheckpointsPath, sessionID) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentCheckpointsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// GetCheckpoint returns a single checkpoint by ID. +func (s *HostedAgentsServiceOp) GetCheckpoint(ctx context.Context, sessionID, checkpointID string) (*HostedAgentCheckpoint, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if checkpointID == "" { + return nil, nil, errors.New("hosted agents: checkpoint id is required") + } + path := fmt.Sprintf(hostedAgentSessionCheckpointByIDPath, sessionID, checkpointID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentCheckpointRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Checkpoint == nil { + return nil, resp, errors.New("hosted agents: get checkpoint returned no checkpoint") + } + return root.Checkpoint, resp, nil +} + +// DeleteCheckpoint removes a checkpoint (DB row + substrate capture). Idempotent: +// deleting an already-gone checkpoint still returns success. +func (s *HostedAgentsServiceOp) DeleteCheckpoint(ctx context.Context, sessionID, checkpointID string) (*HostedAgentCheckpointDeleteResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if checkpointID == "" { + return nil, nil, errors.New("hosted agents: checkpoint id is required") + } + path := fmt.Sprintf(hostedAgentSessionCheckpointByIDPath, sessionID, checkpointID) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentCheckpointDeleteResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ForkSession creates N independent child sessions from a checkpoint (or from +// "now" when FromCheckpointID is empty). Count defaults to 1; max is HostedAgentForkMaxCount. +// The operation is all-or-nothing: on failure nothing is left running. +func (s *HostedAgentsServiceOp) ForkSession(ctx context.Context, sessionID string, fork *HostedAgentForkSessionRequest) (*HostedAgentForkSessionResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if fork == nil { + fork = &HostedAgentForkSessionRequest{} + } + if fork.Count < 0 || fork.Count > HostedAgentForkMaxCount { + return nil, nil, fmt.Errorf("hosted agents: fork count must be between 1 and %d (got %d)", HostedAgentForkMaxCount, fork.Count) + } + path := fmt.Sprintf(hostedAgentSessionForkPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, fork) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentForkSessionResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// RollbackToCheckpoint rewinds the session in place to the given checkpoint. +// The session_id is unchanged; the underlying sandbox is replaced. Returns the +// updated session. +func (s *HostedAgentsServiceOp) RollbackToCheckpoint(ctx context.Context, sessionID, checkpointID string) (*HostedAgentSession, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if checkpointID == "" { + return nil, nil, errors.New("hosted agents: checkpoint id is required") + } + path := fmt.Sprintf(hostedAgentSessionCheckpointRollbackPath, sessionID, checkpointID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, struct{}{}) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentSessionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Session == nil { + return nil, resp, errors.New("hosted agents: rollback returned no session") + } + return root.Session, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/hosted_agent_configs.go b/vendor/github.com/digitalocean/godo/hosted_agent_configs.go new file mode 100644 index 000000000..4e319c008 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/hosted_agent_configs.go @@ -0,0 +1,228 @@ +package godo + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +const ( + hostedAgentsConfigsBasePath = "/v2/agents/configs" + hostedAgentConfigByIDPath = hostedAgentsConfigsBasePath + "/%s" + hostedAgentConfigSessionsPath = hostedAgentConfigByIDPath + "/sessions" +) + +// HostedAgentConfigCredentialSlot is redacted credential metadata on GET/create. +// It echoes only the manifest's declaration for the slot; secret values are +// never returned. Whether a slot is fulfilled is implied by Source: a +// tenantSecret is always stored at create, and an oauth slot is a declaration +// only until oauth brokering is implemented. +// +// Prefer HostedEnvironmentCredentialSlot; this name remains for compatibility. +type HostedAgentConfigCredentialSlot struct { + Name string `json:"name"` + Source string `json:"source,omitempty"` + Provider string `json:"provider,omitempty"` +} + +// HostedAgentConfig is an immutable team-scoped environment definition +// (product name: Environment; historically called Agent Config). +// Prefer HostedEnvironment; this name remains for compatibility. +type HostedAgentConfig struct { + ID string `json:"id"` + Name string `json:"name"` + AgentSpecSchemaVersion string `json:"agentspec_schema_version"` + Manifest json.RawMessage `json:"manifest"` + ContentHash string `json:"content_hash"` + CreatedBy string `json:"created_by"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` + Credentials []HostedAgentConfigCredentialSlot `json:"credentials,omitempty"` + // Warnings carries non-fatal create-time advisories computed from the + // manifest. It is populated on the create response only. + Warnings []string `json:"warnings,omitempty"` +} + +// HostedAgentConfigSummary is the list view (no manifest / credentials). +// Prefer HostedEnvironmentSummary; this name remains for compatibility. +type HostedAgentConfigSummary struct { + ID string `json:"id"` + Name string `json:"name"` + AgentSpecSchemaVersion string `json:"agentspec_schema_version"` + ContentHash string `json:"content_hash"` + CreatedBy string `json:"created_by"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` +} + +// HostedAgentConfigCreateRequest is the body for POST /v2/agents/configs. +// Credentials are not request fields: every secret is declared in the manifest +// under secrets (Environment Spec) or the legacy agents.yaml shape, and a +// tenantSecret slot carries its plaintext in a write-only value that is +// extracted server-side and never persisted or returned. Sending the retired +// secrets or oauth_assignments maps is rejected with a 400. +// Prefer HostedEnvironmentCreateRequest; this name remains for compatibility. +type HostedAgentConfigCreateRequest struct { + Name string `json:"name"` + ManifestYAML string `json:"manifest_yaml"` +} + +// HostedAgentConfigListOptions specifies optional list pagination. +// Prefer HostedEnvironmentListOptions; this name remains for compatibility. +type HostedAgentConfigListOptions struct { + PageToken string `url:"page_token,omitempty"` + PageSize int `url:"page_size,omitempty"` +} + +// HostedAgentConfigsListResponse is returned by GET /v2/agents/configs. +// The JSON field remains "configs" on the wire. Prefer +// HostedEnvironmentsListResponse; this name remains for compatibility. +type HostedAgentConfigsListResponse struct { + Configs []HostedAgentConfigSummary `json:"configs"` + NextPageToken string `json:"next_page_token"` +} + +type hostedAgentConfigRoot struct { + Config *HostedAgentConfig `json:"config"` +} + +// Advertised Environment aliases. Prefer these over the HostedAgentConfig* +// names; the Agent Config identifiers remain as compatible aliases. +type ( + HostedEnvironment = HostedAgentConfig + HostedEnvironmentCredentialSlot = HostedAgentConfigCredentialSlot + HostedEnvironmentSummary = HostedAgentConfigSummary + HostedEnvironmentCreateRequest = HostedAgentConfigCreateRequest + HostedEnvironmentListOptions = HostedAgentConfigListOptions + HostedEnvironmentsListResponse = HostedAgentConfigsListResponse +) + +// ListEnvironments lists active Environments for the caller's team. +func (s *HostedAgentsServiceOp) ListEnvironments(ctx context.Context, opt *HostedEnvironmentListOptions) (*HostedEnvironmentsListResponse, *Response, error) { + return s.ListAgentConfigs(ctx, opt) +} + +// GetEnvironment returns one Environment with redacted credential slots. +func (s *HostedAgentsServiceOp) GetEnvironment(ctx context.Context, environmentID string) (*HostedEnvironment, *Response, error) { + return s.GetAgentConfig(ctx, environmentID) +} + +// CreateEnvironment creates an immutable Environment from a name and +// environment spec YAML. Secret values belong in the manifest's secrets map +// (or legacy agents.yaml secret slots), not as separate request fields. +func (s *HostedAgentsServiceOp) CreateEnvironment(ctx context.Context, create *HostedEnvironmentCreateRequest) (*HostedEnvironment, *Response, error) { + return s.CreateAgentConfig(ctx, create) +} + +// DeleteEnvironment soft-deletes an Environment. The API returns HTTP 204. +func (s *HostedAgentsServiceOp) DeleteEnvironment(ctx context.Context, environmentID string) (*Response, error) { + return s.DeleteAgentConfig(ctx, environmentID) +} + +// ListEnvironmentSessions lists sessions created from an Environment. +func (s *HostedAgentsServiceOp) ListEnvironmentSessions(ctx context.Context, environmentID string, opt *HostedAgentSessionListOptions) (*HostedAgentSessionsListResponse, *Response, error) { + return s.ListAgentConfigSessions(ctx, environmentID, opt) +} + +// ListAgentConfigs lists active Environments for the caller's team. +// Prefer ListEnvironments; this name remains for compatibility. +func (s *HostedAgentsServiceOp) ListAgentConfigs(ctx context.Context, opt *HostedAgentConfigListOptions) (*HostedAgentConfigsListResponse, *Response, error) { + path, err := addOptions(hostedAgentsConfigsBasePath, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentConfigsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// GetAgentConfig returns one Environment with redacted credential slots. +// Prefer GetEnvironment; this name remains for compatibility. +func (s *HostedAgentsServiceOp) GetAgentConfig(ctx context.Context, configID string) (*HostedAgentConfig, *Response, error) { + if configID == "" { + return nil, nil, errors.New("hosted agents: config id is required") + } + path := fmt.Sprintf(hostedAgentConfigByIDPath, configID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentConfigRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Config == nil { + return nil, resp, errors.New("hosted agents: get config returned no config") + } + return root.Config, resp, nil +} + +// CreateAgentConfig creates an immutable Environment from a name and +// environment spec YAML. Prefer CreateEnvironment; this name remains for +// compatibility. Secret values belong in the manifest, not as separate +// request fields. +func (s *HostedAgentsServiceOp) CreateAgentConfig(ctx context.Context, create *HostedAgentConfigCreateRequest) (*HostedAgentConfig, *Response, error) { + if create == nil { + return nil, nil, errors.New("hosted agents: create request is required") + } + req, err := s.client.NewRequest(ctx, http.MethodPost, hostedAgentsConfigsBasePath, create) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentConfigRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Config == nil { + return nil, resp, errors.New("hosted agents: create config returned no config") + } + return root.Config, resp, nil +} + +// DeleteAgentConfig soft-deletes an Environment. The API returns HTTP 204. +// Prefer DeleteEnvironment; this name remains for compatibility. +func (s *HostedAgentsServiceOp) DeleteAgentConfig(ctx context.Context, configID string) (*Response, error) { + if configID == "" { + return nil, errors.New("hosted agents: config id is required") + } + path := fmt.Sprintf(hostedAgentConfigByIDPath, configID) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// ListAgentConfigSessions lists sessions created from an Environment. +// Prefer ListEnvironmentSessions; this name remains for compatibility. +func (s *HostedAgentsServiceOp) ListAgentConfigSessions(ctx context.Context, configID string, opt *HostedAgentSessionListOptions) (*HostedAgentSessionsListResponse, *Response, error) { + if configID == "" { + return nil, nil, errors.New("hosted agents: config id is required") + } + path := fmt.Sprintf(hostedAgentConfigSessionsPath, configID) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentSessionsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/hosted_agent_policy.go b/vendor/github.com/digitalocean/godo/hosted_agent_policy.go new file mode 100644 index 000000000..b95323fcc --- /dev/null +++ b/vendor/github.com/digitalocean/godo/hosted_agent_policy.go @@ -0,0 +1,95 @@ +package godo + +import ( + "bytes" + "context" + "errors" +) + +// HostedAgentPolicyAction is a tool-permission disposition. +type HostedAgentPolicyAction string + +const ( + HostedAgentPolicyActionAllow HostedAgentPolicyAction = "allow" + HostedAgentPolicyActionAsk HostedAgentPolicyAction = "ask" + HostedAgentPolicyActionDeny HostedAgentPolicyAction = "deny" +) + +// HostedAgentPolicyEnforcement controls how strictly a rule is enforced. +type HostedAgentPolicyEnforcement string + +const ( + HostedAgentPolicyEnforcementStrict HostedAgentPolicyEnforcement = "strict" + HostedAgentPolicyEnforcementBestEffort HostedAgentPolicyEnforcement = "best-effort" +) + +// HostedAgentPolicyVerdict is the validation outcome for one resolved rule. +type HostedAgentPolicyVerdict string + +const ( + HostedAgentPolicyVerdictExact HostedAgentPolicyVerdict = "exact" + HostedAgentPolicyVerdictDegraded HostedAgentPolicyVerdict = "degraded" + HostedAgentPolicyVerdictUnsupported HostedAgentPolicyVerdict = "unsupported" + HostedAgentPolicyVerdictUnverified HostedAgentPolicyVerdict = "unverified" + HostedAgentPolicyVerdictDelegated HostedAgentPolicyVerdict = "delegated" +) + +// HostedAgentPolicyResolvedRule is a rule after resolution, carrying a stable +// id for audit correlation. +type HostedAgentPolicyResolvedRule struct { + ID string `json:"id,omitempty"` + Tool string `json:"tool,omitempty"` + Match map[string]string `json:"match,omitempty"` + Action HostedAgentPolicyAction `json:"action,omitempty"` + Enforcement HostedAgentPolicyEnforcement `json:"enforcement,omitempty"` +} + +// HostedAgentPolicyRuleVerdict pairs a resolved rule with its validation outcome. +type HostedAgentPolicyRuleVerdict struct { + Rule HostedAgentPolicyResolvedRule `json:"rule,omitempty"` + Verdict HostedAgentPolicyVerdict `json:"verdict,omitempty"` + Rendered HostedAgentPolicyAction `json:"rendered,omitempty"` + Reason string `json:"reason,omitempty"` + Suggestion string `json:"suggestion,omitempty"` +} + +// HostedAgentPolicyValidationResult is returned by ValidatePolicy. +// The same shape is persisted per session in session_policies.verdicts. +type HostedAgentPolicyValidationResult struct { + Agent string `json:"agent,omitempty"` + Version string `json:"version,omitempty"` + OK bool `json:"ok"` + // DefaultAction is the catch-all disposition as shipped (after unverified + // allow→ask downgrade). When DefaultActionVerdict is degraded, this remains + // the shipped value; see DefaultActionRendered for what the agent enforces. + DefaultAction HostedAgentPolicyAction `json:"defaultAction,omitempty"` + // DefaultActionVerdict classifies how faithfully the agent can enforce the + // catch-all defaultAction. + DefaultActionVerdict HostedAgentPolicyVerdict `json:"defaultActionVerdict,omitempty"` + // DefaultActionRendered is the disposition the agent will actually enforce + // as the catch-all. + DefaultActionRendered HostedAgentPolicyAction `json:"defaultActionRendered,omitempty"` + DefaultActionReason string `json:"defaultActionReason,omitempty"` + Verdicts []HostedAgentPolicyRuleVerdict `json:"verdicts,omitempty"` + // ActionGatewayReason is set when a do.actions tool/toolbelt reference failed + // tool-registry's existence check. + ActionGatewayReason string `json:"actionGatewayReason,omitempty"` +} + +// ValidatePolicy checks a manifest's tool-permission policy against the agent's +// capabilities without creating a session. +func (s *HostedAgentsServiceOp) ValidatePolicy(ctx context.Context, manifest []byte) (*HostedAgentPolicyValidationResult, *Response, error) { + if len(bytes.TrimSpace(manifest)) == 0 { + return nil, nil, errors.New("hosted agents: manifest is required") + } + req, err := s.newCreateSessionPostRequest(ctx, hostedAgentPolicyValidatePath, bytes.NewReader(manifest), hostedAgentManifestMediaType) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentPolicyValidationResult) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/hosted_agent_templates.go b/vendor/github.com/digitalocean/godo/hosted_agent_templates.go new file mode 100644 index 000000000..a51c3b7cb --- /dev/null +++ b/vendor/github.com/digitalocean/godo/hosted_agent_templates.go @@ -0,0 +1,310 @@ +package godo + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +const ( + hostedAgentsTemplatesBasePath = "/v2/agents/templates" + hostedAgentTemplateByIDPath = hostedAgentsTemplatesBasePath + "/%s" + hostedAgentTemplateBuildsPath = hostedAgentTemplateByIDPath + "/builds" + hostedAgentTemplateBuildByIDPath = hostedAgentTemplateBuildsPath + "/%s" + hostedAgentTemplateBuildLogsByIDPath = hostedAgentTemplateBuildByIDPath + "/logs" +) + +// HostedAgentTemplateStatus is the lifecycle status of a team custom template. +type HostedAgentTemplateStatus string + +const ( + HostedAgentTemplateStatusUnspecified HostedAgentTemplateStatus = "TEMPLATE_STATUS_UNSPECIFIED" + HostedAgentTemplateStatusPending HostedAgentTemplateStatus = "TEMPLATE_STATUS_PENDING" + HostedAgentTemplateStatusBuilding HostedAgentTemplateStatus = "TEMPLATE_STATUS_BUILDING" + HostedAgentTemplateStatusReady HostedAgentTemplateStatus = "TEMPLATE_STATUS_READY" + HostedAgentTemplateStatusFailed HostedAgentTemplateStatus = "TEMPLATE_STATUS_FAILED" +) + +// HostedAgentTemplateBuildStatus is the lifecycle status of a template build. +type HostedAgentTemplateBuildStatus string + +const ( + HostedAgentTemplateBuildStatusUnspecified HostedAgentTemplateBuildStatus = "STATUS_UNSPECIFIED" + HostedAgentTemplateBuildStatusPending HostedAgentTemplateBuildStatus = "PENDING" + HostedAgentTemplateBuildStatusBuilding HostedAgentTemplateBuildStatus = "BUILDING" + HostedAgentTemplateBuildStatusSucceeded HostedAgentTemplateBuildStatus = "SUCCEEDED" + HostedAgentTemplateBuildStatusFailed HostedAgentTemplateBuildStatus = "FAILED" +) + +// HostedAgentTemplateImageSource is the customer OCI image used as template input. +type HostedAgentTemplateImageSource struct { + Registry string `json:"registry,omitempty"` + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + Digest string `json:"digest,omitempty"` +} + +// HostedAgentTemplateSpec is the resolved template source. +type HostedAgentTemplateSpec struct { + // BaseTemplate is the platform base key: coding-base | coding-codex | coding-opencode. + BaseTemplate string `json:"base_template,omitempty"` + Image *HostedAgentTemplateImageSource `json:"image,omitempty"` +} + +// HostedAgentTemplate is a team-owned custom sandbox template. +type HostedAgentTemplate struct { + TemplateID string `json:"template_id"` + Name string `json:"name"` + Spec *HostedAgentTemplateSpec `json:"spec,omitempty"` + Status HostedAgentTemplateStatus `json:"status"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` +} + +// HostedAgentTemplateBuild is one immutable build of a template. +type HostedAgentTemplateBuild struct { + BuildID string `json:"build_id"` + TemplateID string `json:"template_id,omitempty"` + Name string `json:"name,omitempty"` + Spec *HostedAgentTemplateSpec `json:"spec,omitempty"` + Status HostedAgentTemplateBuildStatus `json:"status"` + Error string `json:"error,omitempty"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` +} + +// HostedAgentTemplateCreateRequest is the body for POST /v2/agents/templates. +type HostedAgentTemplateCreateRequest struct { + Name string `json:"name"` + BaseTemplate string `json:"base_template"` + SourceOCIRef string `json:"source_oci_ref"` +} + +// HostedAgentTemplateUpdateRequest is the body for PUT /v2/agents/templates/{template_id}. +// Updating source kicks a new build. +type HostedAgentTemplateUpdateRequest struct { + SourceOCIRef string `json:"source_oci_ref,omitempty"` + BaseTemplate string `json:"base_template,omitempty"` +} + +// HostedAgentTemplateListOptions paginates ListTemplates. +type HostedAgentTemplateListOptions struct { + PageToken string `url:"page_token,omitempty"` + PageSize int `url:"page_size,omitempty"` +} + +// HostedAgentTemplatesListResponse is returned by GET /v2/agents/templates. +type HostedAgentTemplatesListResponse struct { + Templates []HostedAgentTemplate `json:"templates"` + NextPageToken string `json:"next_page_token,omitempty"` +} + +// HostedAgentTemplateDeleteResponse is returned by DELETE /v2/agents/templates/{template_id}. +type HostedAgentTemplateDeleteResponse struct { + TemplateID string `json:"template_id"` + Deleted bool `json:"deleted"` +} + +// HostedAgentTemplateBuildListOptions paginates ListTemplateBuilds. +type HostedAgentTemplateBuildListOptions struct { + PageToken string `url:"page_token,omitempty"` + PageSize int `url:"page_size,omitempty"` +} + +// HostedAgentTemplateBuildsListResponse is returned by GET .../builds. +type HostedAgentTemplateBuildsListResponse struct { + Builds []HostedAgentTemplateBuild `json:"builds"` + NextPageToken string `json:"next_page_token,omitempty"` +} + +// HostedAgentTemplateBuildLogs is returned by GET .../builds/{build_id}/logs. +type HostedAgentTemplateBuildLogs struct { + // SignedURL is a short-lived (15-minute) pre-signed Spaces GET URL. + SignedURL string `json:"signed_url"` +} + +type hostedAgentTemplateRoot struct { + Template *HostedAgentTemplate `json:"template"` +} + +type hostedAgentTemplateBuildRoot struct { + Build *HostedAgentTemplateBuild `json:"build"` +} + +// ListTemplates returns the caller's team custom templates. +func (s *HostedAgentsServiceOp) ListTemplates(ctx context.Context, opt *HostedAgentTemplateListOptions) (*HostedAgentTemplatesListResponse, *Response, error) { + path, err := addOptions(hostedAgentsTemplatesBasePath, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTemplatesListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// CreateTemplate creates a team custom template and kicks a build. +func (s *HostedAgentsServiceOp) CreateTemplate(ctx context.Context, create *HostedAgentTemplateCreateRequest) (*HostedAgentTemplate, *Response, error) { + if create == nil { + return nil, nil, errors.New("hosted agents: create template request is required") + } + if create.Name == "" { + return nil, nil, errors.New("hosted agents: name is required") + } + if create.BaseTemplate == "" { + return nil, nil, errors.New("hosted agents: base_template is required") + } + if create.SourceOCIRef == "" { + return nil, nil, errors.New("hosted agents: source_oci_ref is required") + } + req, err := s.client.NewRequest(ctx, http.MethodPost, hostedAgentsTemplatesBasePath, create) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTemplateRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Template == nil { + return nil, resp, errors.New("hosted agents: create template returned no template") + } + return root.Template, resp, nil +} + +// GetTemplate returns a team custom template by public id. +func (s *HostedAgentsServiceOp) GetTemplate(ctx context.Context, templateID string) (*HostedAgentTemplate, *Response, error) { + if templateID == "" { + return nil, nil, errors.New("hosted agents: template id is required") + } + path := fmt.Sprintf(hostedAgentTemplateByIDPath, templateID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTemplateRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Template == nil { + return nil, resp, errors.New("hosted agents: get template returned no template") + } + return root.Template, resp, nil +} + +// UpdateTemplate updates a team template's source and kicks a new build. +func (s *HostedAgentsServiceOp) UpdateTemplate(ctx context.Context, templateID string, update *HostedAgentTemplateUpdateRequest) (*HostedAgentTemplate, *Response, error) { + if templateID == "" { + return nil, nil, errors.New("hosted agents: template id is required") + } + if update == nil { + return nil, nil, errors.New("hosted agents: update template request is required") + } + path := fmt.Sprintf(hostedAgentTemplateByIDPath, templateID) + req, err := s.client.NewRequest(ctx, http.MethodPut, path, update) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTemplateRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Template == nil { + return nil, resp, errors.New("hosted agents: update template returned no template") + } + return root.Template, resp, nil +} + +// DeleteTemplate deletes a team custom template. +func (s *HostedAgentsServiceOp) DeleteTemplate(ctx context.Context, templateID string) (*HostedAgentTemplateDeleteResponse, *Response, error) { + if templateID == "" { + return nil, nil, errors.New("hosted agents: template id is required") + } + path := fmt.Sprintf(hostedAgentTemplateByIDPath, templateID) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTemplateDeleteResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ListTemplateBuilds returns build history for a template. +func (s *HostedAgentsServiceOp) ListTemplateBuilds(ctx context.Context, templateID string, opt *HostedAgentTemplateBuildListOptions) (*HostedAgentTemplateBuildsListResponse, *Response, error) { + if templateID == "" { + return nil, nil, errors.New("hosted agents: template id is required") + } + path := fmt.Sprintf(hostedAgentTemplateBuildsPath, templateID) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTemplateBuildsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// GetTemplateBuild returns a single template build. +func (s *HostedAgentsServiceOp) GetTemplateBuild(ctx context.Context, templateID, buildID string) (*HostedAgentTemplateBuild, *Response, error) { + if templateID == "" { + return nil, nil, errors.New("hosted agents: template id is required") + } + if buildID == "" { + return nil, nil, errors.New("hosted agents: build id is required") + } + path := fmt.Sprintf(hostedAgentTemplateBuildByIDPath, templateID, buildID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTemplateBuildRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Build == nil { + return nil, resp, errors.New("hosted agents: get template build returned no build") + } + return root.Build, resp, nil +} + +// GetTemplateBuildLogs returns a short-lived signed URL for archived build logs. +func (s *HostedAgentsServiceOp) GetTemplateBuildLogs(ctx context.Context, templateID, buildID string) (*HostedAgentTemplateBuildLogs, *Response, error) { + if templateID == "" { + return nil, nil, errors.New("hosted agents: template id is required") + } + if buildID == "" { + return nil, nil, errors.New("hosted agents: build id is required") + } + path := fmt.Sprintf(hostedAgentTemplateBuildLogsByIDPath, templateID, buildID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTemplateBuildLogs) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/hosted_agent_triggers.go b/vendor/github.com/digitalocean/godo/hosted_agent_triggers.go new file mode 100644 index 000000000..7c8788903 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/hosted_agent_triggers.go @@ -0,0 +1,536 @@ +package godo + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +const ( + hostedAgentTriggersBasePath = "/v2/agents/triggers" + hostedAgentTriggerByIDPath = hostedAgentTriggersBasePath + "/%s" + hostedAgentTriggerRotateSecretPath = hostedAgentTriggerByIDPath + "/rotate-secret" + hostedAgentTriggerExecutionsPath = hostedAgentTriggerByIDPath + "/executions" + hostedAgentTriggerExecutionByIDPath = hostedAgentTriggerExecutionsPath + "/%s" + hostedAgentTriggerBySessionPath = hostedAgentTriggersBasePath + "/by-session/%s" + hostedAgentReusableSessionsPath = hostedAgentTriggersBasePath + "/reusable-sessions" + hostedAgentWebhookProvidersPath = "/v2/agents/webhook-providers" +) + +// HostedAgentTriggersService exposes the Hosted Agents Triggers public REST API +// (webhook & cron triggers for agent runs) under /v2/agents/triggers and +// /v2/agents/webhook-providers. +type HostedAgentTriggersService interface { + List(context.Context, *HostedAgentTriggerListOptions) (*HostedAgentTriggersListResponse, *Response, error) + Create(context.Context, *HostedAgentTriggerCreateRequest) (*HostedAgentTriggerCreateResponse, *Response, error) + Get(context.Context, string) (*HostedAgentTrigger, *Response, error) + Update(context.Context, string, *HostedAgentTriggerUpdateRequest) (*HostedAgentTrigger, *Response, error) + Delete(context.Context, string) (*Response, error) + RotateSecret(context.Context, string, *HostedAgentTriggerRotateSecretOptions) (*HostedAgentTriggerRotateSecretResponse, *Response, error) + ListExecutions(context.Context, string, *HostedAgentTriggerExecutionListOptions) (*HostedAgentTriggerExecutionsListResponse, *Response, error) + GetExecution(context.Context, string, string) (*HostedAgentTriggerExecution, *Response, error) + GetBySession(context.Context, string) (*HostedAgentTrigger, *Response, error) + ListReusableSessions(context.Context, *HostedAgentReusableSessionListOptions) (*HostedAgentReusableSessionsListResponse, *Response, error) + ListWebhookProviders(context.Context) (*HostedAgentWebhookProvidersListResponse, *Response, error) +} + +// HostedAgentTriggersServiceOp handles communication with Hosted Agents Trigger methods. +type HostedAgentTriggersServiceOp struct { + client *Client +} + +var _ HostedAgentTriggersService = &HostedAgentTriggersServiceOp{} + +// HostedAgentTriggerKind is how a trigger fires. +type HostedAgentTriggerKind string + +const ( + HostedAgentTriggerKindWebhook HostedAgentTriggerKind = "webhook" + HostedAgentTriggerKindCron HostedAgentTriggerKind = "cron" +) + +// HostedAgentTriggerStatus is the trigger lifecycle state. +type HostedAgentTriggerStatus string + +const ( + HostedAgentTriggerStatusActive HostedAgentTriggerStatus = "active" + HostedAgentTriggerStatusPaused HostedAgentTriggerStatus = "paused" +) + +// HostedAgentTriggerSessionMode controls whether each firing creates a new +// session or reuses a bound paused session. +type HostedAgentTriggerSessionMode string + +const ( + HostedAgentTriggerSessionModeFresh HostedAgentTriggerSessionMode = "fresh" + HostedAgentTriggerSessionModeReuse HostedAgentTriggerSessionMode = "reuse" +) + +// HostedAgentTriggerOutputMode is how run output is delivered. +type HostedAgentTriggerOutputMode string + +const ( + HostedAgentTriggerOutputModeNone HostedAgentTriggerOutputMode = "none" + HostedAgentTriggerOutputModeEmail HostedAgentTriggerOutputMode = "email" + // HostedAgentTriggerOutputModeSlack delivers run output to a Slack incoming webhook. + HostedAgentTriggerOutputModeSlack HostedAgentTriggerOutputMode = "slack" +) + +// HostedAgentWebhookProviderKey identifies the signature-verification scheme. +type HostedAgentWebhookProviderKey string + +const ( + HostedAgentWebhookProviderGitHub HostedAgentWebhookProviderKey = "github" + HostedAgentWebhookProviderGitLab HostedAgentWebhookProviderKey = "gitlab" + HostedAgentWebhookProviderCustom HostedAgentWebhookProviderKey = "custom" +) + +// HostedAgentTriggerExecutionStatus is the per-firing outcome. +type HostedAgentTriggerExecutionStatus string + +const ( + HostedAgentTriggerExecutionStatusPending HostedAgentTriggerExecutionStatus = "pending" + HostedAgentTriggerExecutionStatusRunning HostedAgentTriggerExecutionStatus = "running" + HostedAgentTriggerExecutionStatusSucceeded HostedAgentTriggerExecutionStatus = "succeeded" + HostedAgentTriggerExecutionStatusFailed HostedAgentTriggerExecutionStatus = "failed" +) + +// HostedAgentWebhookSignatureScheme is how an external system authenticates a delivery. +type HostedAgentWebhookSignatureScheme string + +const ( + HostedAgentWebhookSignatureHMACSHA256 HostedAgentWebhookSignatureScheme = "hmac-sha256" + HostedAgentWebhookSignaturePlaintext HostedAgentWebhookSignatureScheme = "plaintext" +) + +// HostedAgentTrigger is a webhook or cron automation bound to an agent/session. +type HostedAgentTrigger struct { + TriggerID string `json:"trigger_id,omitempty"` + TeamID int64 `json:"team_id,omitempty"` + Kind HostedAgentTriggerKind `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Status HostedAgentTriggerStatus `json:"status,omitempty"` + SessionMode HostedAgentTriggerSessionMode `json:"session_mode,omitempty"` + AgentKind HostedAgentKind `json:"agent_kind,omitempty"` + PromptTemplate string `json:"prompt_template,omitempty"` + Output *HostedAgentTriggerOutputRead `json:"output,omitempty"` + SessionTemplate string `json:"session_template,omitempty"` + BoundSessionID string `json:"bound_session_id,omitempty"` + Webhook *HostedAgentWebhookConfig `json:"webhook,omitempty"` + Cron *HostedAgentCronConfig `json:"cron,omitempty"` + CreatedAt Timestamp `json:"created_at,omitempty"` + UpdatedAt Timestamp `json:"updated_at,omitempty"` +} + +// HostedAgentTriggerOutputRead is output-delivery config as returned on reads. +// Email addresses and Slack webhook URLs are write-only and never returned. +type HostedAgentTriggerOutputRead struct { + Mode HostedAgentTriggerOutputMode `json:"mode,omitempty"` + EmailConfigured bool `json:"email_configured,omitempty"` + SlackConfigured bool `json:"slack_configured,omitempty"` +} + +// HostedAgentTriggerOutputWrite is output-delivery config on create/update. +// Email is required when mode=email; Slack is required when mode=slack. +// Destinations are stored encrypted and never returned on reads. +type HostedAgentTriggerOutputWrite struct { + Mode HostedAgentTriggerOutputMode `json:"mode"` + Email string `json:"email,omitempty"` + Slack *HostedAgentTriggerSlackOutputWrite `json:"slack,omitempty"` +} + +// HostedAgentTriggerSlackOutputWrite is the Slack destination on create/update. +type HostedAgentTriggerSlackOutputWrite struct { + WebhookURL string `json:"webhook_url"` +} + +// HostedAgentWebhookConfig is present when kind=webhook. +type HostedAgentWebhookConfig struct { + Provider HostedAgentWebhookProviderKey `json:"provider,omitempty"` + WebhookURL string `json:"webhook_url,omitempty"` +} + +// HostedAgentCronConfig is present when kind=cron. +type HostedAgentCronConfig struct { + CronExpr string `json:"cron_expr,omitempty"` + Timezone string `json:"timezone,omitempty"` + NextRunAt Timestamp `json:"next_run_at,omitempty"` +} + +// HostedAgentTriggerCreateRequest creates a webhook or cron trigger. +type HostedAgentTriggerCreateRequest struct { + Kind HostedAgentTriggerKind `json:"kind"` + Name string `json:"name"` + SessionMode HostedAgentTriggerSessionMode `json:"session_mode"` + PromptTemplate string `json:"prompt_template"` + Output HostedAgentTriggerOutputWrite `json:"output"` + SessionTemplate string `json:"session_template,omitempty"` + BoundSessionID string `json:"bound_session_id,omitempty"` + Webhook *HostedAgentCreateWebhookConfig `json:"webhook,omitempty"` + Cron *HostedAgentCreateCronConfig `json:"cron,omitempty"` +} + +// HostedAgentCreateWebhookConfig is the webhook block on create. +type HostedAgentCreateWebhookConfig struct { + Provider HostedAgentWebhookProviderKey `json:"provider,omitempty"` +} + +// HostedAgentCreateCronConfig is the cron block on create/update. +type HostedAgentCreateCronConfig struct { + CronExpr string `json:"cron_expr"` + Timezone string `json:"timezone"` +} + +// HostedAgentTriggerUpdateRequest is a partial update (pause/re-enable via status). +type HostedAgentTriggerUpdateRequest struct { + Name string `json:"name,omitempty"` + Status HostedAgentTriggerStatus `json:"status,omitempty"` + PromptTemplate string `json:"prompt_template,omitempty"` + Output *HostedAgentTriggerOutputWrite `json:"output,omitempty"` + SessionTemplate string `json:"session_template,omitempty"` + BoundSessionID string `json:"bound_session_id,omitempty"` + Cron *HostedAgentCreateCronConfig `json:"cron,omitempty"` +} + +// HostedAgentTriggerCreateResponse is returned on create. +// WebhookSecret is present for webhook triggers only and shown exactly once. +type HostedAgentTriggerCreateResponse struct { + Trigger *HostedAgentTrigger `json:"trigger,omitempty"` + WebhookSecret string `json:"webhook_secret,omitempty"` +} + +// HostedAgentTriggerListOptions specifies optional list filters and pagination. +type HostedAgentTriggerListOptions struct { + PageSize int `url:"page_size,omitempty"` + PageToken string `url:"page_token,omitempty"` + Kind HostedAgentTriggerKind `url:"kind,omitempty"` + Status HostedAgentTriggerStatus `url:"status,omitempty"` +} + +// HostedAgentTriggersListResponse is returned by List. +type HostedAgentTriggersListResponse struct { + Triggers []HostedAgentTrigger `json:"triggers"` + NextPageToken string `json:"next_page_token,omitempty"` +} + +// HostedAgentTriggerRotateSecretOptions specifies optional rotation behaviour. +// A nil options pointer, or GracePeriodSeconds left nil, selects the server +// default (5 minutes). Pass a pointer to 0 to revoke the outgoing secret +// immediately; any positive value is a custom handoff window in seconds +// (server-enforced max, default 1 hour). +type HostedAgentTriggerRotateSecretOptions struct { + // GracePeriodSeconds is how long the outgoing secret keeps verifying. + // Nil omits the query parameter (server default). Non-nil 0 revokes now. + GracePeriodSeconds *int `url:"grace_period_seconds,omitempty"` +} + +// HostedAgentTriggerRotateSecretResponse is returned by RotateSecret. +// Exactly one of PreviousSecretExpiresAt and PreviousSecretRevoked is set, so a +// caller never has to infer the outcome from a missing field. +type HostedAgentTriggerRotateSecretResponse struct { + WebhookSecret string `json:"webhook_secret,omitempty"` + // PreviousSecretExpiresAt is when the outgoing secret stops verifying + // deliveries. Nil when the secret was revoked on the rotate call. + PreviousSecretExpiresAt *Timestamp `json:"previous_secret_expires_at,omitempty"` + // PreviousSecretRevoked reports that the outgoing secret is already dead. + PreviousSecretRevoked bool `json:"previous_secret_revoked,omitempty"` +} + +// HostedAgentTriggerExecution is one row per firing. +type HostedAgentTriggerExecution struct { + ExecutionID string `json:"execution_id,omitempty"` + TriggerID string `json:"trigger_id,omitempty"` + Status HostedAgentTriggerExecutionStatus `json:"status,omitempty"` + SessionID string `json:"session_id,omitempty"` + RunID string `json:"run_id,omitempty"` + FailureReason string `json:"failure_reason,omitempty"` + CreatedAt Timestamp `json:"created_at,omitempty"` + UpdatedAt Timestamp `json:"updated_at,omitempty"` + Payload string `json:"payload,omitempty"` + OutputText string `json:"output_text,omitempty"` + OutputTruncated bool `json:"output_truncated,omitempty"` +} + +// HostedAgentTriggerExecutionListOptions specifies optional execution list filters. +type HostedAgentTriggerExecutionListOptions struct { + PageSize int `url:"page_size,omitempty"` + PageToken string `url:"page_token,omitempty"` + Status HostedAgentTriggerExecutionStatus `url:"status,omitempty"` +} + +// HostedAgentTriggerExecutionsListResponse is returned by ListExecutions. +type HostedAgentTriggerExecutionsListResponse struct { + Executions []HostedAgentTriggerExecution `json:"executions"` + NextPageToken string `json:"next_page_token,omitempty"` +} + +// HostedAgentWebhookProviderSignature describes how a provider signs deliveries. +type HostedAgentWebhookProviderSignature struct { + Header string `json:"header,omitempty"` + Scheme HostedAgentWebhookSignatureScheme `json:"scheme,omitempty"` +} + +// HostedAgentWebhookProvider is a static registry entry for the create-trigger UI. +type HostedAgentWebhookProvider struct { + Key HostedAgentWebhookProviderKey `json:"key,omitempty"` + DisplayName string `json:"display_name,omitempty"` + Description string `json:"description,omitempty"` + DocsURL string `json:"docs_url,omitempty"` + PasteHint string `json:"paste_hint,omitempty"` + Signature *HostedAgentWebhookProviderSignature `json:"signature,omitempty"` +} + +// HostedAgentWebhookProvidersListResponse is returned by ListWebhookProviders. +type HostedAgentWebhookProvidersListResponse struct { + Providers []HostedAgentWebhookProvider `json:"providers"` +} + +// HostedAgentReusableSession is a paused session suitable for session_mode=reuse. +type HostedAgentReusableSession struct { + SessionID string `json:"session_id,omitempty"` + Name string `json:"name,omitempty"` + AgentKind HostedAgentKind `json:"agent_kind,omitempty"` + Status HostedAgentSessionStatus `json:"status,omitempty"` + CreatedAt Timestamp `json:"created_at,omitempty"` + LastEventAt Timestamp `json:"last_event_at,omitempty"` +} + +// HostedAgentReusableSessionListOptions specifies pagination for reusable sessions. +type HostedAgentReusableSessionListOptions struct { + PageSize int `url:"page_size,omitempty"` + PageToken string `url:"page_token,omitempty"` +} + +// HostedAgentReusableSessionsListResponse is returned by ListReusableSessions. +type HostedAgentReusableSessionsListResponse struct { + Sessions []HostedAgentReusableSession `json:"sessions"` + NextPageToken string `json:"next_page_token,omitempty"` +} + +type hostedAgentTriggerRoot struct { + Trigger *HostedAgentTrigger `json:"trigger"` +} + +type hostedAgentTriggerExecutionRoot struct { + Execution *HostedAgentTriggerExecution `json:"execution"` +} + +// List enumerates the calling team's triggers. +func (s *HostedAgentTriggersServiceOp) List(ctx context.Context, opt *HostedAgentTriggerListOptions) (*HostedAgentTriggersListResponse, *Response, error) { + path, err := addOptions(hostedAgentTriggersBasePath, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTriggersListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// Create creates a webhook or cron trigger. +// For webhook triggers, WebhookSecret is returned exactly once. +func (s *HostedAgentTriggersServiceOp) Create(ctx context.Context, create *HostedAgentTriggerCreateRequest) (*HostedAgentTriggerCreateResponse, *Response, error) { + if create == nil { + return nil, nil, NewArgError("create", "cannot be nil") + } + req, err := s.client.NewRequest(ctx, http.MethodPost, hostedAgentTriggersBasePath, create) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTriggerCreateResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// Get returns a single trigger by ID. +func (s *HostedAgentTriggersServiceOp) Get(ctx context.Context, triggerID string) (*HostedAgentTrigger, *Response, error) { + if triggerID == "" { + return nil, nil, errors.New("hosted agent triggers: trigger id is required") + } + path := fmt.Sprintf(hostedAgentTriggerByIDPath, triggerID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTriggerRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Trigger == nil { + return nil, resp, errors.New("hosted agent triggers: get returned no trigger") + } + return root.Trigger, resp, nil +} + +// Update partially updates, pauses, or re-enables a trigger. +func (s *HostedAgentTriggersServiceOp) Update(ctx context.Context, triggerID string, update *HostedAgentTriggerUpdateRequest) (*HostedAgentTrigger, *Response, error) { + if triggerID == "" { + return nil, nil, errors.New("hosted agent triggers: trigger id is required") + } + if update == nil { + return nil, nil, NewArgError("update", "cannot be nil") + } + path := fmt.Sprintf(hostedAgentTriggerByIDPath, triggerID) + req, err := s.client.NewRequest(ctx, http.MethodPatch, path, update) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTriggerRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Trigger == nil { + return nil, resp, errors.New("hosted agent triggers: update returned no trigger") + } + return root.Trigger, resp, nil +} + +// Delete soft-deletes a trigger. The API returns HTTP 204 on success. +func (s *HostedAgentTriggersServiceOp) Delete(ctx context.Context, triggerID string) (*Response, error) { + if triggerID == "" { + return nil, errors.New("hosted agent triggers: trigger id is required") + } + path := fmt.Sprintf(hostedAgentTriggerByIDPath, triggerID) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// RotateSecret issues a new webhook secret (webhook triggers only). +// By default (nil options, or GracePeriodSeconds left nil) the outgoing secret +// keeps verifying for the server default window (5 minutes) and +// PreviousSecretExpiresAt reports when it dies. Pass GracePeriodSeconds pointing +// at 0 to revoke it on this call, or at a positive value for a custom window. +func (s *HostedAgentTriggersServiceOp) RotateSecret(ctx context.Context, triggerID string, opt *HostedAgentTriggerRotateSecretOptions) (*HostedAgentTriggerRotateSecretResponse, *Response, error) { + if triggerID == "" { + return nil, nil, errors.New("hosted agent triggers: trigger id is required") + } + path := fmt.Sprintf(hostedAgentTriggerRotateSecretPath, triggerID) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodPost, path, struct{}{}) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTriggerRotateSecretResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ListExecutions returns a trigger's execution history (payload omitted from list items). +func (s *HostedAgentTriggersServiceOp) ListExecutions(ctx context.Context, triggerID string, opt *HostedAgentTriggerExecutionListOptions) (*HostedAgentTriggerExecutionsListResponse, *Response, error) { + if triggerID == "" { + return nil, nil, errors.New("hosted agent triggers: trigger id is required") + } + path := fmt.Sprintf(hostedAgentTriggerExecutionsPath, triggerID) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentTriggerExecutionsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// GetExecution returns a single execution, including its payload. +func (s *HostedAgentTriggersServiceOp) GetExecution(ctx context.Context, triggerID, executionID string) (*HostedAgentTriggerExecution, *Response, error) { + if triggerID == "" { + return nil, nil, errors.New("hosted agent triggers: trigger id is required") + } + if executionID == "" { + return nil, nil, errors.New("hosted agent triggers: execution id is required") + } + path := fmt.Sprintf(hostedAgentTriggerExecutionByIDPath, triggerID, executionID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTriggerExecutionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Execution == nil { + return nil, resp, errors.New("hosted agent triggers: get execution returned no execution") + } + return root.Execution, resp, nil +} + +// GetBySession reverse-looks-up the trigger that produced or binds a session. +func (s *HostedAgentTriggersServiceOp) GetBySession(ctx context.Context, sessionID string) (*HostedAgentTrigger, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agent triggers: session id is required") + } + path := fmt.Sprintf(hostedAgentTriggerBySessionPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentTriggerRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Trigger == nil { + return nil, resp, errors.New("hosted agent triggers: get by session returned no trigger") + } + return root.Trigger, resp, nil +} + +// ListReusableSessions lists the team's PAUSED sessions for the reuse picker. +func (s *HostedAgentTriggersServiceOp) ListReusableSessions(ctx context.Context, opt *HostedAgentReusableSessionListOptions) (*HostedAgentReusableSessionsListResponse, *Response, error) { + path, err := addOptions(hostedAgentReusableSessionsPath, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentReusableSessionsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ListWebhookProviders lists supported webhook providers for the create-trigger UI. +func (s *HostedAgentTriggersServiceOp) ListWebhookProviders(ctx context.Context) (*HostedAgentWebhookProvidersListResponse, *Response, error) { + req, err := s.client.NewRequest(ctx, http.MethodGet, hostedAgentWebhookProvidersPath, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentWebhookProvidersListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/hosted_agents.go b/vendor/github.com/digitalocean/godo/hosted_agents.go new file mode 100644 index 000000000..2cbd008de --- /dev/null +++ b/vendor/github.com/digitalocean/godo/hosted_agents.go @@ -0,0 +1,1778 @@ +package godo + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "hash" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +const ( + hostedAgentManifestMediaType = "application/x-yaml" + hostedAgentWorkspaceMediaType = "application/octet-stream" + + hostedAgentsSessionsBasePath = "/v2/agents/sessions" + hostedAgentPolicyValidatePath = hostedAgentsSessionsBasePath + "/policy/validate" + hostedAgentSessionByIDPath = hostedAgentsSessionsBasePath + "/%s" + hostedAgentSessionEventsPath = hostedAgentSessionByIDPath + "/events" + hostedAgentSessionInputPath = hostedAgentSessionByIDPath + "/input" + hostedAgentSessionRequestPath = hostedAgentSessionByIDPath + "/request" + hostedAgentSessionHITLPath = hostedAgentSessionByIDPath + "/hitl/%s" + hostedAgentSessionSandboxExecPath = hostedAgentSessionByIDPath + "/sandbox/exec" + hostedAgentSandboxSizesPath = hostedAgentsSessionsBasePath + "/sandbox/sizes" + hostedAgentSessionPausePath = hostedAgentSessionByIDPath + "/pause" + hostedAgentSessionResumePath = hostedAgentSessionByIDPath + "/resume" + hostedAgentSessionWorkspaceUploadPath = hostedAgentSessionByIDPath + "/workspace/upload" + hostedAgentSessionWorkspaceDownloadPath = hostedAgentSessionByIDPath + "/workspace/download" + hostedAgentSessionWorkspaceTransfersPath = hostedAgentSessionByIDPath + "/workspace/transfers" + hostedAgentSessionWorkspaceTransferByIDPath = hostedAgentSessionWorkspaceTransfersPath + "/%s" + hostedAgentSessionWorkspaceTransferPartURLsPath = hostedAgentSessionWorkspaceTransferByIDPath + "/part-upload-urls" + hostedAgentSessionWorkspaceTransferCommitPath = hostedAgentSessionWorkspaceTransferByIDPath + "/commit" + hostedAgentSessionWorkspaceTransferCancelPath = hostedAgentSessionWorkspaceTransferByIDPath + "/cancel" + + // Team-scoped external-provider (e.g. GitHub) OAuth connect flow. + hostedAgentProviderAuthPath = "/v2/agents/auth/%s" + hostedAgentProviderAuthPollPath = hostedAgentProviderAuthPath + "/poll" + + hostedAgentSessionCheckpointsPath = hostedAgentSessionByIDPath + "/checkpoints" + hostedAgentSessionCheckpointByIDPath = hostedAgentSessionCheckpointsPath + "/%s" + hostedAgentSessionCheckpointRollbackPath = hostedAgentSessionCheckpointByIDPath + "/rollback" + hostedAgentSessionForkPath = hostedAgentSessionByIDPath + "/fork" + + // HostedAgentForkMaxCount is the v1 cap on children created by one fork call. + HostedAgentForkMaxCount = 4 + + workspaceContentSHA256Header = "X-Content-Sha256" + workspaceIsArchiveHeader = "X-Workspace-Is-Archive" + workspaceSizeBytesHeader = "X-Workspace-Size-Bytes" + + // sseLastEventIDHeader is the standard SSE resume cursor. The data-plane + // events endpoint takes the cursor here rather than as a query parameter. + sseLastEventIDHeader = "Last-Event-ID" + + // workspaceDownloadFooter is appended by OHS after a successful download + // payload so integrity survives intermediaries that strip HTTP trailers + // (e.g. Cloudflare). Format: DOWSSHA1 + 64 lowercase hex + '\n' = 73 bytes. + workspaceDownloadFooterPrefix = "DOWSSHA1" + workspaceDownloadFooterLen = len(workspaceDownloadFooterPrefix) + 64 + 1 +) + +// HostedAgentsService exposes the DigitalOcean Hosted Agents session API +// (HarnessAPI from harness.proto). Routes live under /v2/agents/sessions. +type HostedAgentsService interface { + // CreateSession provisions a session using the legacy JSON body (agent_kind, + // repo_hint, idle_timeout_seconds). Prefer CreateSessionFromManifest for + // Environment Spec YAML (or CreateSessionFromEnvironment for a saved + // Environment). + CreateSession(context.Context, *HostedAgentSessionCreateRequest) (*HostedAgentSession, *Response, error) + // CreateSessionFromManifest uploads an environment spec YAML document + // (Content-Type: application/x-yaml). Legacy agents.digitalocean.com/v1alpha1 + // Agent manifests are also accepted where the API still supports them. + // For OpenAI sandbox-provider sessions (adapter codex-agentapi), pass + // OpenAISessionID in opt so harness-api can persist openai_session_id for + // attach correlation (?openai_session_id=). doctl resolves ${...} + // placeholders client-side before calling this API; there is no + // server-side variables map. + CreateSessionFromManifest(context.Context, []byte, *HostedAgentManifestCreateOptions) (*HostedAgentSession, *Response, error) + // CreateSessionFromEnvironment provisions a session from an existing + // immutable Environment, referenced by config_id. The request uses + // Content-Type: application/json with a {name, config_id} body (optional + // resume_on_topoff); the environment's stored manifest and declared + // credentials seed the session at create time. + CreateSessionFromEnvironment(context.Context, *HostedAgentSessionFromEnvironmentRequest) (*HostedAgentSession, *Response, error) + // CreateSessionFromConfig is an alias for CreateSessionFromEnvironment. + // Prefer CreateSessionFromEnvironment; this name remains for compatibility. + CreateSessionFromConfig(context.Context, *HostedAgentSessionFromConfigRequest) (*HostedAgentSession, *Response, error) + // ValidatePolicy checks a manifest's tool-permission policy against the + // agent's capabilities without creating a session. Manifest is the same YAML + // document accepted by CreateSessionFromManifest (Content-Type: + // application/x-yaml). Returns per-rule verdicts; OK is false when any strict + // rule is unsupported or unverified (i.e. CreateSession would reject it). + ValidatePolicy(context.Context, []byte) (*HostedAgentPolicyValidationResult, *Response, error) + ListSessions(context.Context, *HostedAgentSessionListOptions) (*HostedAgentSessionsListResponse, *Response, error) + GetSession(context.Context, string) (*HostedAgentSession, *Response, error) + // UpdateSession patches session-scoped settings on an existing session and + // returns the updated snapshot. Never touches the agent manifest or config. + UpdateSession(context.Context, string, *HostedAgentSessionUpdateRequest) (*HostedAgentSession, *Response, error) + DestroySession(context.Context, string) (*Response, error) + PauseSession(context.Context, string) (*Response, error) + ResumeSession(context.Context, string) (*Response, error) + StreamSession(context.Context, string, *HostedAgentSessionStreamOptions) (*HostedAgentSessionStream, *Response, error) + SendInput(context.Context, string, *HostedAgentSendInputRequest) (*HostedAgentSendInputResponse, *Response, error) + RelayRequest(context.Context, string, *HostedAgentRelayRequest) (*HostedAgentRelayResponse, *Response, error) + ResolveHITL(context.Context, string, string, *HostedAgentResolveHITLRequest) (*Response, error) + + // StartProviderAuth begins (or resumes) the team-scoped connect flow for an + // external provider (e.g. "github"). The team is derived from the + // authenticated principal; there is no request body. + StartProviderAuth(context.Context, string) (*HostedAgentProviderAuthStart, *Response, error) + // PollProviderAuth reports whether a pending connect link has been + // authorized. pollURL is the PollURL returned by StartProviderAuth. + PollProviderAuth(context.Context, string, string) (*HostedAgentProviderAuthPoll, *Response, error) + ExecInSandbox(context.Context, string, *HostedAgentSandboxExecRequest) (*HostedAgentSandboxExecResponse, *Response, error) + // ListSandboxSizes returns the customer-selectable sandbox (microVM) size + // catalog (GET /v2/agents/sessions/sandbox/sizes). Every slug is accepted by + // CreateSession as spec.sandbox.sizeSlug. Ordered smallest-to-largest; no + // pagination. + ListSandboxSizes(context.Context) (*HostedAgentSandboxSizesResponse, *Response, error) + UploadWorkspace(context.Context, string, *HostedAgentWorkspaceUploadRequest) (*HostedAgentWorkspaceUploadResponse, *Response, error) + DownloadWorkspace(context.Context, string, *HostedAgentWorkspaceDownloadRequest) (*HostedAgentWorkspaceDownload, *Response, error) + + // Large-file (>~50 MiB) staged workspace transfer APIs. Streaming + // UploadWorkspace / DownloadWorkspace remain for smaller payloads. + CreateWorkspaceTransfer(context.Context, string, *HostedAgentWorkspaceTransferCreateRequest) (*HostedAgentWorkspaceTransfer, *Response, error) + CreateWorkspaceTransferPartUploadURLs(context.Context, string, string, *HostedAgentWorkspaceTransferPartUploadURLsRequest) (*HostedAgentWorkspaceTransferPartUploadURLs, *Response, error) + CommitWorkspaceTransfer(context.Context, string, string, *HostedAgentWorkspaceTransferCommitRequest) (*HostedAgentWorkspaceTransfer, *Response, error) + GetWorkspaceTransfer(context.Context, string, string) (*HostedAgentWorkspaceTransfer, *Response, error) + CancelWorkspaceTransfer(context.Context, string, string, *HostedAgentWorkspaceTransferCancelRequest) (*HostedAgentWorkspaceTransferCancelResponse, *Response, error) + + // Checkpoint / fork / rollback (session save points). + CreateCheckpoint(context.Context, string, *HostedAgentCheckpointCreateRequest) (*HostedAgentCheckpoint, *Response, error) + ListCheckpoints(context.Context, string, *HostedAgentCheckpointListOptions) (*HostedAgentCheckpointsListResponse, *Response, error) + GetCheckpoint(context.Context, string, string) (*HostedAgentCheckpoint, *Response, error) + DeleteCheckpoint(context.Context, string, string) (*HostedAgentCheckpointDeleteResponse, *Response, error) + ForkSession(context.Context, string, *HostedAgentForkSessionRequest) (*HostedAgentForkSessionResponse, *Response, error) + RollbackToCheckpoint(context.Context, string, string) (*HostedAgentSession, *Response, error) + + // Environments (immutable team-scoped environment definitions). Routes + // live under /v2/agents/configs. Prefer the Environment* methods; the + // AgentConfig* names remain as compatible aliases. + ListEnvironments(context.Context, *HostedEnvironmentListOptions) (*HostedEnvironmentsListResponse, *Response, error) + GetEnvironment(context.Context, string) (*HostedEnvironment, *Response, error) + CreateEnvironment(context.Context, *HostedEnvironmentCreateRequest) (*HostedEnvironment, *Response, error) + DeleteEnvironment(context.Context, string) (*Response, error) + ListEnvironmentSessions(context.Context, string, *HostedAgentSessionListOptions) (*HostedAgentSessionsListResponse, *Response, error) + // Prefer ListEnvironments / GetEnvironment / CreateEnvironment / + // DeleteEnvironment / ListEnvironmentSessions. + ListAgentConfigs(context.Context, *HostedAgentConfigListOptions) (*HostedAgentConfigsListResponse, *Response, error) + GetAgentConfig(context.Context, string) (*HostedAgentConfig, *Response, error) + CreateAgentConfig(context.Context, *HostedAgentConfigCreateRequest) (*HostedAgentConfig, *Response, error) + DeleteAgentConfig(context.Context, string) (*Response, error) + ListAgentConfigSessions(context.Context, string, *HostedAgentSessionListOptions) (*HostedAgentSessionsListResponse, *Response, error) + + // Team custom templates (BYOA). Routes live under /v2/agents/templates. + ListTemplates(context.Context, *HostedAgentTemplateListOptions) (*HostedAgentTemplatesListResponse, *Response, error) + CreateTemplate(context.Context, *HostedAgentTemplateCreateRequest) (*HostedAgentTemplate, *Response, error) + GetTemplate(context.Context, string) (*HostedAgentTemplate, *Response, error) + UpdateTemplate(context.Context, string, *HostedAgentTemplateUpdateRequest) (*HostedAgentTemplate, *Response, error) + DeleteTemplate(context.Context, string) (*HostedAgentTemplateDeleteResponse, *Response, error) + ListTemplateBuilds(context.Context, string, *HostedAgentTemplateBuildListOptions) (*HostedAgentTemplateBuildsListResponse, *Response, error) + GetTemplateBuild(context.Context, string, string) (*HostedAgentTemplateBuild, *Response, error) + GetTemplateBuildLogs(context.Context, string, string) (*HostedAgentTemplateBuildLogs, *Response, error) +} + +// HostedAgentsServiceOp handles communication with Hosted Agents session methods. +type HostedAgentsServiceOp struct { + client *Client +} + +var _ HostedAgentsService = &HostedAgentsServiceOp{} + +// HostedAgentKind identifies the agent runtime provisioned for a session. +type HostedAgentKind string + +const ( + HostedAgentKindUnspecified HostedAgentKind = "AGENT_KIND_UNSPECIFIED" + HostedAgentKindClaudeCode HostedAgentKind = "AGENT_KIND_CLAUDE_CODE" + HostedAgentKindOpenCode HostedAgentKind = "AGENT_KIND_OPENCODE" + HostedAgentKindCodexCLI HostedAgentKind = "AGENT_KIND_CODEX_CLI" + HostedAgentKindCursorCLI HostedAgentKind = "AGENT_KIND_CURSOR_CLI" + // HostedAgentKindOpenAICodex is the OpenAI Agents API sandbox-provider kind + // (adapter codex-agentapi). Attach bridges to OpenAI, not DO's SSE stream. + HostedAgentKindOpenAICodex HostedAgentKind = "AGENT_KIND_OPENAI_CODEX" + HostedAgentKindNone HostedAgentKind = "AGENT_KIND_NONE" + HostedAgentKindCustom HostedAgentKind = "AGENT_KIND_CUSTOM" +) + +// HostedAgentSessionStatus is the lifecycle status of a hosted agent session. +type HostedAgentSessionStatus string + +const ( + HostedAgentSessionStatusUnspecified HostedAgentSessionStatus = "SESSION_STATUS_UNSPECIFIED" + HostedAgentSessionStatusProvisioning HostedAgentSessionStatus = "SESSION_STATUS_PROVISIONING" + HostedAgentSessionStatusReady HostedAgentSessionStatus = "SESSION_STATUS_READY" + HostedAgentSessionStatusDetached HostedAgentSessionStatus = "SESSION_STATUS_DETACHED" + HostedAgentSessionStatusDestroying HostedAgentSessionStatus = "SESSION_STATUS_DESTROYING" + HostedAgentSessionStatusDestroyed HostedAgentSessionStatus = "SESSION_STATUS_DESTROYED" + HostedAgentSessionStatusFailed HostedAgentSessionStatus = "SESSION_STATUS_FAILED" + HostedAgentSessionStatusPaused HostedAgentSessionStatus = "SESSION_STATUS_PAUSED" +) + +// HostedAgentSessionPauseReason is why a paused session was paused. The API +// reserves the right to add reasons, so an unrecognized value is an opaque +// reason string rather than an error: compare against the constants below to +// recognize a reason, but pass anything else through as-is. +type HostedAgentSessionPauseReason string + +const ( + // HostedAgentSessionPauseReasonManual is a pause requested through PauseSession. + HostedAgentSessionPauseReasonManual HostedAgentSessionPauseReason = "manual" + // HostedAgentSessionPauseReasonIdle is an automatic pause after inactivity. + HostedAgentSessionPauseReasonIdle HostedAgentSessionPauseReason = "idle" + // HostedAgentSessionPauseReasonZeroBalance is a pause by the prepayment gate. + // It is the only reason the server auto-resumes from, and only for sessions + // with ResumeOnTopoff set. + HostedAgentSessionPauseReasonZeroBalance HostedAgentSessionPauseReason = "zero_balance" + // Deprecated: Use HostedAgentSessionPauseReasonZeroBalance. Servers now return + // "zero_balance"; this constant is kept for backward compatibility with older API responses. + HostedAgentSessionPauseReasonLowBalance HostedAgentSessionPauseReason = "low_balance" +) + +// HostedAgentProviderAuthState tracks OAuth authorization for an external provider. +type HostedAgentProviderAuthState string + +const ( + HostedAgentProviderAuthStateUnspecified HostedAgentProviderAuthState = "PROVIDER_AUTH_STATE_UNSPECIFIED" + HostedAgentProviderAuthStateNone HostedAgentProviderAuthState = "PROVIDER_AUTH_STATE_NONE" + HostedAgentProviderAuthStatePending HostedAgentProviderAuthState = "PROVIDER_AUTH_STATE_PENDING" + HostedAgentProviderAuthStateAuthorized HostedAgentProviderAuthState = "PROVIDER_AUTH_STATE_AUTHORIZED" + HostedAgentProviderAuthStateExpired HostedAgentProviderAuthState = "PROVIDER_AUTH_STATE_EXPIRED" +) + +// HostedAgentRunState is the lifecycle state of a single agent run. +type HostedAgentRunState string + +const ( + HostedAgentRunStateUnspecified HostedAgentRunState = "RUN_STATE_UNSPECIFIED" + HostedAgentRunStateQueued HostedAgentRunState = "RUN_STATE_QUEUED" + HostedAgentRunStateRunning HostedAgentRunState = "RUN_STATE_RUNNING" + HostedAgentRunStateAwaitingHITL HostedAgentRunState = "RUN_STATE_AWAITING_HITL" + HostedAgentRunStatePaused HostedAgentRunState = "RUN_STATE_PAUSED" + HostedAgentRunStateCompleted HostedAgentRunState = "RUN_STATE_COMPLETED" + HostedAgentRunStateFailed HostedAgentRunState = "RUN_STATE_FAILED" +) + +// HostedAgentHITLActionKind classifies a human-in-the-loop approval request. +type HostedAgentHITLActionKind string + +const ( + HostedAgentHITLActionUnspecified HostedAgentHITLActionKind = "HITL_ACTION_KIND_UNSPECIFIED" + HostedAgentHITLActionBash HostedAgentHITLActionKind = "HITL_ACTION_BASH" + HostedAgentHITLActionFileWriteOutsideWorkspace HostedAgentHITLActionKind = "HITL_ACTION_FILE_WRITE_OUTSIDE_WORKSPACE" + HostedAgentHITLActionGitHubCommitPush HostedAgentHITLActionKind = "HITL_ACTION_GITHUB_COMMIT_PUSH" + HostedAgentHITLActionGitHubCreatePR HostedAgentHITLActionKind = "HITL_ACTION_GITHUB_CREATE_PR" + HostedAgentHITLActionGitHubBranchDelete HostedAgentHITLActionKind = "HITL_ACTION_GITHUB_BRANCH_DELETE" + HostedAgentHITLActionGitHubForcePush HostedAgentHITLActionKind = "HITL_ACTION_GITHUB_FORCE_PUSH" +) + +// HostedAgentHITLOutcome is the user's decision on a HITL request. +type HostedAgentHITLOutcome string + +const ( + HostedAgentHITLOutcomeUnspecified HostedAgentHITLOutcome = "HITL_OUTCOME_UNSPECIFIED" + HostedAgentHITLOutcomeApprove HostedAgentHITLOutcome = "HITL_OUTCOME_APPROVE" + HostedAgentHITLOutcomeReject HostedAgentHITLOutcome = "HITL_OUTCOME_REJECT" + HostedAgentHITLOutcomeDefer HostedAgentHITLOutcome = "HITL_OUTCOME_DEFER" +) + +// HostedAgentResolutionSource records where a HITL decision originated. +type HostedAgentResolutionSource string + +const ( + HostedAgentResolutionSourceUnspecified HostedAgentResolutionSource = "RESOLUTION_SOURCE_UNSPECIFIED" + HostedAgentResolutionSourceInlineKeystroke HostedAgentResolutionSource = "RESOLUTION_SOURCE_INLINE_KEYSTROKE" + HostedAgentResolutionSourceOutOfBand HostedAgentResolutionSource = "RESOLUTION_SOURCE_OUT_OF_BAND" +) + +// HostedAgentRunFailureCode classifies a failed run. +type HostedAgentRunFailureCode string + +const ( + HostedAgentRunFailureCodeUnspecified HostedAgentRunFailureCode = "RUN_FAILURE_CODE_UNSPECIFIED" + HostedAgentRunFailureCodeModelError HostedAgentRunFailureCode = "RUN_FAILURE_CODE_MODEL_ERROR" + HostedAgentRunFailureCodeModelTimeout HostedAgentRunFailureCode = "RUN_FAILURE_CODE_MODEL_TIMEOUT" + HostedAgentRunFailureCodeToolError HostedAgentRunFailureCode = "RUN_FAILURE_CODE_TOOL_ERROR" + HostedAgentRunFailureCodeSandboxLost HostedAgentRunFailureCode = "RUN_FAILURE_CODE_SANDBOX_LOST" + HostedAgentRunFailureCodeHITLRejected HostedAgentRunFailureCode = "RUN_FAILURE_CODE_HITL_REJECTED" + HostedAgentRunFailureCodeBudgetExceeded HostedAgentRunFailureCode = "RUN_FAILURE_CODE_BUDGET_EXCEEDED" + HostedAgentRunFailureCodeInternal HostedAgentRunFailureCode = "RUN_FAILURE_CODE_INTERNAL" +) + +// HostedAgentEventKind is the SSE event discriminator for session stream +// payloads. The values are the canonical SPI event type names (dot-separated) +// emitted on the wire's `type` field — NOT proto enum names. They mirror the +// spi.EventType constants owned by the hosted-agents stack. +type HostedAgentEventKind string + +const ( + HostedAgentEventKindUnspecified HostedAgentEventKind = "" + HostedAgentEventKindRunStarted HostedAgentEventKind = "run.started" + HostedAgentEventKindTokenChunk HostedAgentEventKind = "run.token_delta" + HostedAgentEventKindToolCallStarted HostedAgentEventKind = "run.tool_call_started" + HostedAgentEventKindToolCallCompleted HostedAgentEventKind = "run.tool_call_completed" + HostedAgentEventKindHITLRequested HostedAgentEventKind = "run.human_input_requested" + HostedAgentEventKindHITLResolved HostedAgentEventKind = "run.human_input_received" + HostedAgentEventKindRunCompleted HostedAgentEventKind = "run.completed" + HostedAgentEventKindRunFailed HostedAgentEventKind = "run.failed" + HostedAgentEventKindRunPaused HostedAgentEventKind = "run.paused" + HostedAgentEventKindRunResumed HostedAgentEventKind = "run.resumed" + HostedAgentEventKindSessionUpdated HostedAgentEventKind = "session.updated" + HostedAgentEventKindRunStateCheckpointed HostedAgentEventKind = "run.state_checkpointed" + HostedAgentEventKindRunHandoff HostedAgentEventKind = "run.handoff" + HostedAgentEventKindRunUsageRecorded HostedAgentEventKind = "run.usage_recorded" + HostedAgentEventKindRunSandboxAllocated HostedAgentEventKind = "run.sandbox_allocated" + HostedAgentEventKindRunSandboxReleased HostedAgentEventKind = "run.sandbox_released" + HostedAgentEventKindRunCostAccrued HostedAgentEventKind = "run.cost_accrued" + HostedAgentEventKindRunLog HostedAgentEventKind = "run.log" + + // HostedAgentEventKindStreamState is a transport control frame, not an agent + // event: it reports the health of the SSE connection itself. Only the + // data-plane events endpoint emits it. It arrives in the same envelope as an + // event, but only SessionID, At and Payload are meaningful — decode Payload + // with HostedAgentStreamState. Renderers should skip it rather than display + // it as session activity. + HostedAgentEventKindStreamState HostedAgentEventKind = "stream.state" +) + +// HostedAgentStreamState is the payload of a HostedAgentEventKindStreamState +// frame: the current health of the SSE connection. +type HostedAgentStreamState struct { + State HostedAgentStreamStateValue `json:"state"` + Cursor string `json:"cursor,omitempty"` +} + +// HostedAgentStreamStateValue enumerates the stream health states. +type HostedAgentStreamStateValue string + +const ( + // HostedAgentStreamStateLive means events are being delivered contiguously. + HostedAgentStreamStateLive HostedAgentStreamStateValue = "live" + // HostedAgentStreamStateCatchingUp means the connection is replaying recent + // history before it joins the live tail. + HostedAgentStreamStateCatchingUp HostedAgentStreamStateValue = "catching_up" + // HostedAgentStreamStateDegraded means delivery continues with reduced + // guarantees (the server fell back to polling). + HostedAgentStreamStateDegraded HostedAgentStreamStateValue = "degraded" + // HostedAgentStreamStateSuperseded means a newer connection from the same + // device took over. The server closes this stream; the client should stop + // rather than reconnect, or the two connections will evict each other. + HostedAgentStreamStateSuperseded HostedAgentStreamStateValue = "superseded" +) + +// HostedAgentSessionOriginProduct identifies the product workflow that created +// a session. Wire values match harness SessionOrigin.product. +type HostedAgentSessionOriginProduct string + +const ( + HostedAgentSessionOriginProductDirect HostedAgentSessionOriginProduct = "direct" + HostedAgentSessionOriginProductSimulation HostedAgentSessionOriginProduct = "simulation" + HostedAgentSessionOriginProductEvaluation HostedAgentSessionOriginProduct = "evaluation" +) + +// HostedAgentSessionOriginRequest is the product-provenance claim on create. +// Omission creates a verified direct session. Simulation and evaluation require +// resource_id; direct forbids it. verified is server-assigned and must not be +// sent on create (see HostedAgentSessionOrigin). +type HostedAgentSessionOriginRequest struct { + Product HostedAgentSessionOriginProduct `json:"product"` + ResourceID string `json:"resource_id,omitempty"` +} + +// HostedAgentSessionOrigin is server-returned product-workflow provenance. +// Verified is true for direct sessions and for product claims established +// through a trusted adapter. +type HostedAgentSessionOrigin struct { + Product HostedAgentSessionOriginProduct `json:"product"` + ResourceID string `json:"resource_id,omitempty"` + Verified bool `json:"verified"` +} + +// HostedAgentSession is a provisioned hosted-agent sandbox session. +type HostedAgentSession struct { + SessionID string `json:"session_id"` + Name string `json:"name,omitempty"` + AgentKind HostedAgentKind `json:"agent_kind"` + Status HostedAgentSessionStatus `json:"status"` + CreatedAt Timestamp `json:"created_at"` + LastEventAt Timestamp `json:"last_event_at"` + RepoHint string `json:"repo_hint,omitempty"` + ProviderAuth map[string]HostedAgentProviderAuthState `json:"provider_auth,omitempty"` + // Origin is present for newly created sessions (including direct). Older + // sessions may omit it. + Origin *HostedAgentSessionOrigin `json:"origin,omitempty"` + // PauseReason qualifies Status when it is SESSION_STATUS_PAUSED, and is + // omitted otherwise. Unrecognized values are opaque, not errors — see + // HostedAgentSessionPauseReason. + PauseReason HostedAgentSessionPauseReason `json:"pause_reason,omitempty"` + // SandboxID is the sandbox identity for support / debugging. Informational; + // omitted until a sandbox is allocated. + SandboxID string `json:"sandbox_id,omitempty"` + // ResumeOnTopoff is the consent to auto-resume this session when a + // prepayment balance is restored. Set at create (manifest query param or + // config JSON body), toggled afterwards with UpdateSession, and returned on + // get/list so clients can confirm enrollment. Never inherited by forks. + // Omitted when false. + ResumeOnTopoff bool `json:"resume_on_topoff,omitempty"` + // OpenAISessionID is the OpenAI Agents session id (sess_…) linked to this DO + // sandbox for AGENT_KIND_OPENAI_CODEX. Used by attach to bridge to OpenAI; + // omitempty for other agent kinds. + OpenAISessionID string `json:"openai_session_id,omitempty"` + // OpenAIEnvironmentID is captured from the resolved CODEX_ENVIRONMENT_ID + // guest env value at create. Non-secret correlation metadata. + OpenAIEnvironmentID string `json:"openai_environment_id,omitempty"` + // ParentSessionID is set on forked child sessions; empty/omitted for roots. + ParentSessionID string `json:"parent_session_id,omitempty"` + // ForkID is a branch label on forked sessions; empty/omitted for roots. + ForkID string `json:"fork_id,omitempty"` + // ConfigID is the durable Environment UUID the session was created from + // (inline manifests persist an environment too, named after the session). + // Returned on create/get/list; omitted for sessions created before + // environment references were stored. Historically called Agent Config. + ConfigID string `json:"config_id,omitempty"` + // Warnings carries non-fatal create-time advisories from the server + // (manifest parse + policy fidelity). Populated on the create response + // only; omitted on get/list. + Warnings []string `json:"warnings,omitempty"` +} + +// HostedAgentRun represents a single execution within a session. +type HostedAgentRun struct { + RunID string `json:"run_id"` + SessionID string `json:"session_id"` + State HostedAgentRunState `json:"state"` + StartedAt Timestamp `json:"started_at"` + EndedAt *Timestamp `json:"ended_at,omitempty"` + CumulativeCostMicros int64 `json:"cumulative_cost_micros"` +} + +// HostedAgentHITLRequest is a pending human-in-the-loop approval. +type HostedAgentHITLRequest struct { + RequestID string `json:"request_id"` + SessionID string `json:"session_id"` + RunID string `json:"run_id"` + Action HostedAgentHITLActionKind `json:"action"` + Details map[string]interface{} `json:"details,omitempty"` + Workdir string `json:"workdir,omitempty"` + Deadline *Timestamp `json:"deadline,omitempty"` +} + +// HostedAgentHITLDecision records a resolved HITL request. +type HostedAgentHITLDecision struct { + RequestID string `json:"request_id"` + Outcome HostedAgentHITLOutcome `json:"outcome"` + Actor string `json:"actor,omitempty"` + At Timestamp `json:"at"` + Reason string `json:"reason,omitempty"` +} + +// HostedAgentEvent is one SSE payload from a session stream (see StreamSession). +// +// The server serializes the SPI canonical event envelope, whose JSON shape +// differs from this struct's field names: the discriminator is `type` (not +// `kind`), the per-kind body is `data` (not `payload`), and the timestamp is +// `timestamp` (not `at`). UnmarshalJSON maps that wire shape onto these fields, +// so callers read Kind/Payload/At directly. +type HostedAgentEvent struct { + EventID string + SessionID string + RunID string + Seq uint64 + At Timestamp + Kind HostedAgentEventKind + Payload json.RawMessage + + // SourceEventID is the native event id from the agent runtime before + // canonical mapping (Event.source_event_id). Empty when the runtime does + // not supply stable ids or the server does not forward it. + SourceEventID string + // SourceEventType is the native event type label from the agent runtime + // (e.g. codex's "item/agentMessage/delta"). Empty when not forwarded. + SourceEventType string + // SourceRaw is the exact native event bytes the in-sandbox adapter + // captured before canonical mapping (Event.source_raw) — for codex, one + // JSON-RPC frame as read off the app-server transport. Only present when + // the stream was opened with HostedAgentSessionStreamOptions.IncludeRaw + // and the server retained the bytes; base64 on the wire (JSON []byte). + SourceRaw []byte +} + +// hostedAgentEventWire is the on-the-wire SPI canonical event envelope. +type hostedAgentEventWire struct { + EventID string `json:"event_id"` + RunID string `json:"run_id"` + SessionID string `json:"session_id"` + Timestamp Timestamp `json:"timestamp"` + Seq uint64 `json:"seq"` + SourceEventID string `json:"source_event_id,omitempty"` + SourceEventType string `json:"source_event_type,omitempty"` + SourceRaw []byte `json:"source_raw,omitempty"` + Type HostedAgentEventKind `json:"type"` + Data json.RawMessage `json:"data"` +} + +// UnmarshalJSON decodes the SPI canonical event wire shape. +func (e *HostedAgentEvent) UnmarshalJSON(b []byte) error { + var w hostedAgentEventWire + if err := json.Unmarshal(b, &w); err != nil { + return err + } + e.EventID = w.EventID + e.RunID = w.RunID + e.SessionID = w.SessionID + e.Seq = w.Seq + e.At = w.Timestamp + e.Kind = w.Type + e.Payload = w.Data + e.SourceEventID = w.SourceEventID + e.SourceEventType = w.SourceEventType + e.SourceRaw = w.SourceRaw + return nil +} + +// HostedAgentSessionCreateRequest is the body for POST /v2/agents/sessions. +type HostedAgentSessionCreateRequest struct { + AgentKind HostedAgentKind `json:"agent_kind"` + RepoHint string `json:"repo_hint,omitempty"` + IdleTimeoutSeconds int64 `json:"idle_timeout_seconds,omitempty"` + // Origin claims product-workflow provenance. Omit for a verified direct + // session. Simulation/evaluation require resource_id. + Origin *HostedAgentSessionOriginRequest `json:"origin,omitempty"` +} + +// HostedAgentSessionFromConfigRequest creates a session from an existing +// immutable Environment (POST /v2/agents/sessions with a JSON body). Name is +// required and becomes the new session name; ConfigID must be the UUID of an +// Environment owned by the caller's team. +// Prefer HostedAgentSessionFromEnvironmentRequest; this name remains for +// compatibility. The JSON field remains "config_id" on the wire. +type HostedAgentSessionFromConfigRequest struct { + Name string `json:"name"` + ConfigID string `json:"config_id"` + // ResumeOnTopoff opts the new session in to automatic resumption when the + // team's prepayment balance is restored. Session-scoped, not + // environment-scoped: the referenced Environment never confers it, so one + // shared Environment cannot enrol every session created from it. Omitted + // when false, which leaves the server default (also false). + ResumeOnTopoff bool `json:"resume_on_topoff,omitempty"` +} + +// HostedAgentSessionFromEnvironmentRequest is the advertised alias for +// HostedAgentSessionFromConfigRequest. +type HostedAgentSessionFromEnvironmentRequest = HostedAgentSessionFromConfigRequest + +// HostedAgentSessionUpdateRequest is the body for PATCH +// /v2/agents/sessions/{session_id}: an allowlisted subset of session-scoped +// settings, never an agents.yaml or Agent Config mutation. The server rejects +// unknown top-level keys, and rejects an empty object, so every field is a +// pointer: nil omits it and leaves the server-side value untouched. +type HostedAgentSessionUpdateRequest struct { + // ResumeOnTopoff sets whether the session opts in to automatic resumption + // once the team's prepayment balance is restored after a low-balance pause. + // Nil leaves the current consent unchanged. Revoking takes effect + // immediately for later sweeps; granting it does not by itself resume a + // session that is already paused. + ResumeOnTopoff *bool `json:"resume_on_topoff,omitempty"` +} + +// empty reports whether the request would serialize to {}, which the server +// rejects with a 400. Checked client-side so a no-op call fails locally with a +// useful message instead of spending a round-trip to earn a certain error. +func (r *HostedAgentSessionUpdateRequest) empty() bool { + return r == nil || r.ResumeOnTopoff == nil +} + +// HostedAgentManifestCreateOptions configures CreateSessionFromManifest. +// OpenAISessionID is sent as the openai_session_id query parameter (not in the +// YAML body). harness-api persists it for AGENT_KIND_OPENAI_CODEX attach +// correlation. See docs/design/openai-sandbox-provider.md. +type HostedAgentManifestCreateOptions struct { + OpenAISessionID string `url:"openai_session_id,omitempty"` + // ResumeOnTopoff opts the new session in to automatic resumption when the + // team's prepayment balance is restored after a low-balance pause. It is a + // query parameter rather than an agents.yaml field because that document is + // immutable and shared through Agent Configs, while spending consent has to + // stay per-session. Omitted when false (the server default). + ResumeOnTopoff bool `url:"resume_on_topoff,omitempty"` +} + +// HostedAgentSessionListOptions specifies optional list filters. +type HostedAgentSessionListOptions struct { + PageToken string `url:"page_token,omitempty"` + PageSize int `url:"page_size,omitempty"` + Status HostedAgentSessionStatus `url:"status,omitempty"` + Name string `url:"name,omitempty"` + // ParentSessionID lists child (forked) sessions of the given parent. + ParentSessionID string `url:"parent_session_id,omitempty"` +} + +// HostedAgentSessionsListResponse is returned by GET /v2/agents/sessions. +type HostedAgentSessionsListResponse struct { + Sessions []HostedAgentSession `json:"sessions"` + NextPageToken string `json:"next_page_token"` +} + +// HostedAgentSessionStreamOptions configures the session SSE stream. +// +// ReplayOnly selects between the two modes StreamSession can open on the events +// endpoint (see StreamSession); Before and Limit page backwards through history +// within the replay-only mode. +type HostedAgentSessionStreamOptions struct { + // ReplayFrom is the resume cursor: the id of the last event the caller + // already rendered. On the live stream it is sent as Last-Event-ID; on a + // ReplayOnly read it is sent as the replay_from query parameter. + ReplayFrom string + // ReplayOnly reads the stored event history and ends the stream at the last + // stored event instead of holding the connection open for live events. + ReplayOnly bool + + // Before turns the request into a single backward page of durable + // history: the events strictly older than this event id, oldest-first, + // after which the stream closes without live events. Set it to the + // oldest event id already held. Requires ReplayOnly, since a live + // attach cannot start in the past. + // + // A cursorless replay only covers the newest window of a session's + // history (the server's replay budget), so walking backwards from its + // oldest event is how older history is read. HasMore reports whether + // the walk can continue. + Before string + + // Limit caps the events in one history page. Only meaningful with + // Before. Zero leaves the server's default (200); the server also caps + // any request at its replay budget. + Limit int + + // IncludeRaw asks the server to include each event's native source bytes + // (HostedAgentEvent.SourceRaw) alongside the canonical payload. Raw + // payloads meaningfully fatten every event, so this is opt-in; consumers + // that don't translate native protocols should leave it off. + IncludeRaw bool +} + +// HostedAgentSendInputRequest is the body for POST .../input. +type HostedAgentSendInputRequest struct { + Text string `json:"text"` + + // SourceRaw optionally carries the client's exact native protocol frame + // this input was extracted from — for codex, the TUI's turn/start + // JSON-RPC message with its full params. The in-sandbox adapter uses it + // as the template for the turn it drives, so client intent beyond plain + // text (input items, model, effort, approval policy, ...) survives the + // text reduction. Only meaningful when the caller speaks the session's + // own agent protocol; Text stays required either way. Inbound + // counterpart of HostedAgentEvent.SourceRaw. Base64 on the wire per the + // proto bytes JSON mapping — encoding/json does that for []byte. + SourceRaw []byte `json:"source_raw,omitempty"` +} + +// HostedAgentSendInputResponse is returned by POST .../input. +type HostedAgentSendInputResponse struct { + RunID string `json:"run_id"` +} + +// HostedAgentRelayRequest is the body for POST .../request: one native +// agent-protocol request frame, forwarded to the session's agent verbatim. +// +// Where SendInput carries the one message with a canonical meaning ("the user +// said something"), this carries everything else a client that speaks the +// session's own protocol needs to ask — for codex, the requests behind +// interrupts, slash commands, and model pickers. The control plane never +// parses the frame; only the in-sandbox adapter decides what is safe to +// forward. +type HostedAgentRelayRequest struct { + // SourceRaw is the caller's native protocol request frame — for codex, a + // single JSON-RPC request object carrying the caller's own id. Named to + // match SendInput's field: both mean "my own frame, verbatim". Base64 on + // the wire per the proto bytes JSON mapping. + SourceRaw []byte `json:"source_raw"` +} + +// HostedAgentRelayResponse is the reply to POST .../request. +type HostedAgentRelayResponse struct { + // SourceRaw is the agent's reply frame, addressed to the caller's own + // request id but otherwise verbatim. A protocol-level failure (a JSON-RPC + // error object) is a normal reply and arrives here rather than as an HTTP + // error. + // + // Empty means the in-sandbox adapter declined to forward the method. + // Callers must answer their own caller on that case rather than waiting + // for something that will not come. + SourceRaw []byte `json:"source_raw,omitempty"` +} + +// HostedAgentResolveHITLRequest is the body for POST .../hitl/{requestID}. +type HostedAgentResolveHITLRequest struct { + Outcome HostedAgentHITLOutcome `json:"outcome"` + Reason string `json:"reason,omitempty"` + Source HostedAgentResolutionSource `json:"source,omitempty"` + + // SourceRaw is the client's reply in the agent's own protocol, forwarded + // to the in-sandbox agent untouched. It carries what Outcome cannot: an + // elicitation's content, a tool's requested input, a scope beyond this one + // call. Outcome stays required alongside it — it is what the audit trail + // records, and what the agent falls back to when this is absent. + SourceRaw []byte `json:"source_raw,omitempty"` + + // Content is a structured answer for a gated request that asks for DATA + // rather than just a verdict — an MCP *form* elicitation sends a + // requestedSchema and expects an object matching it back (e.g. + // {"site_url": "https://acme.atlassian.net"} for a Jira connection). Its + // keys are the requesting server's, never fixed here. Omitted for plain + // approvals, where Outcome alone is the whole answer. Mirrors + // harness-api's resolveHITLBody.Content; forwarded to the run and never + // persisted (form content can carry user-entered secrets). + Content map[string]any `json:"content,omitempty"` +} + +// HostedAgentProviderAuthStart is returned by POST /v2/agents/auth/{provider}. +// Status is "pending" when the user must still authorize in a browser +// (ConnectURL/PollURL/VerificationCode are set), or "success" when the team is +// already connected (those fields are empty). It is a free-form connect-flow +// status, distinct from the HostedAgentProviderAuthState session field. The +// authorization handle is never exposed: tokens are exchanged server-side at +// session time. +type HostedAgentProviderAuthStart struct { + Provider string `json:"provider"` + Status string `json:"status"` + ConnectURL string `json:"connect_url,omitempty"` + PollURL string `json:"poll_url,omitempty"` + VerificationCode string `json:"verification_code,omitempty"` + ExpiresAt *Timestamp `json:"expires_at,omitempty"` +} + +// HostedAgentProviderAuthPoll is returned by GET +// /v2/agents/auth/{provider}/poll. It reports only whether authorization has +// completed ("pending" or "success"); no secret is returned. +type HostedAgentProviderAuthPoll struct { + Provider string `json:"provider"` + Status string `json:"status"` + ExpiresAt *Timestamp `json:"expires_at,omitempty"` +} + +// HostedAgentSandboxExecRequest is the body for POST .../sandbox/exec. +type HostedAgentSandboxExecRequest struct { + Argv []string `json:"argv"` + Workdir string `json:"workdir,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +// HostedAgentSandboxExecResponse is returned by POST .../sandbox/exec. +type HostedAgentSandboxExecResponse struct { + ExitCode int `json:"exit_code"` + Stdout string `json:"stdout,omitempty"` + Stderr string `json:"stderr,omitempty"` +} + +// HostedAgentSandboxSize is a customer-selectable sandbox (microVM) size from +// GET /v2/agents/sessions/sandbox/sizes. Submit Slug as spec.sandbox.sizeSlug +// on CreateSession; VCPUs and MemoryMB are for display. +type HostedAgentSandboxSize struct { + Slug string `json:"slug,omitempty"` + VCPUs int `json:"vcpus,omitempty"` + MemoryMB int `json:"memory_mb,omitempty"` +} + +// HostedAgentSandboxSizesResponse is returned by ListSandboxSizes. +type HostedAgentSandboxSizesResponse struct { + // Sizes is ordered smallest-to-largest. + Sizes []HostedAgentSandboxSize `json:"sizes"` +} + +// HostedAgentWorkspaceUploadRequest is the input for UploadWorkspace. +type HostedAgentWorkspaceUploadRequest struct { + // Path is the destination resolved inside the workspace root (/workspace). Required. + Path string + // IsArchive indicates Body is a tar archive to extract at Path. + IsArchive bool + // ContentSHA256 is an optional hex SHA-256 digest of the payload, forwarded to the guest for verification. + ContentSHA256 string + // Body is the raw file or tar bytes to upload. Required. + Body io.Reader +} + +// HostedAgentWorkspaceUploadResponse is returned by UploadWorkspace. +type HostedAgentWorkspaceUploadResponse struct { + Path string `json:"path"` + BytesWritten int64 `json:"bytes_written"` +} + +// HostedAgentWorkspaceDownloadRequest is the input for DownloadWorkspace. +type HostedAgentWorkspaceDownloadRequest struct { + // Path is the source resolved inside the workspace root (/workspace). Required. + Path string + // AsArchive tar-streams the directory at Path when true. + AsArchive bool +} + +// HostedAgentWorkspaceDownload is the streaming result of DownloadWorkspace. +// Body strips the trailing integrity footer and verifies SHA-256 of the +// payload: read it to EOF and then Close it. A missing, invalid, or mismatched +// footer is an error. An HTTP X-Content-Sha256 trailer may still be present +// but is best-effort only; the body footer is the source of truth. +type HostedAgentWorkspaceDownload struct { + Body io.ReadCloser + // IsArchive is true when the payload is a tar stream. + IsArchive bool + // SizeBytes is the X-Workspace-Size-Bytes hint (0 when unknown); payload + // size only (excludes the integrity footer). Not a Content-Length. + SizeBytes int64 +} + +// HostedAgentWorkspaceTransferDirection is the direction of a staged transfer. +type HostedAgentWorkspaceTransferDirection string + +const ( + HostedAgentWorkspaceTransferDirectionUpload HostedAgentWorkspaceTransferDirection = "upload" + HostedAgentWorkspaceTransferDirectionDownload HostedAgentWorkspaceTransferDirection = "download" +) + +// HostedAgentWorkspaceTransferStatus is the status of a staged transfer. +type HostedAgentWorkspaceTransferStatus string + +const ( + HostedAgentWorkspaceTransferStatusPending HostedAgentWorkspaceTransferStatus = "pending" + HostedAgentWorkspaceTransferStatusInProgress HostedAgentWorkspaceTransferStatus = "in_progress" + HostedAgentWorkspaceTransferStatusCompleted HostedAgentWorkspaceTransferStatus = "completed" + HostedAgentWorkspaceTransferStatusFailed HostedAgentWorkspaceTransferStatus = "failed" +) + +// HostedAgentWorkspaceTransferCreateRequest starts a large upload or download. +// Upload uses IsArchive; download uses AsArchive (per OHS HTTP contract). +type HostedAgentWorkspaceTransferCreateRequest struct { + Direction HostedAgentWorkspaceTransferDirection `json:"direction"` + Path string `json:"path"` + IsArchive bool `json:"is_archive,omitempty"` + AsArchive bool `json:"as_archive,omitempty"` + SizeBytes int64 `json:"size_bytes,omitempty"` + SHA256 string `json:"sha256,omitempty"` +} + +// HostedAgentWorkspaceTransfer is returned by create/commit/get transfer calls. +type HostedAgentWorkspaceTransfer struct { + TransferID string `json:"transfer_id"` + Direction HostedAgentWorkspaceTransferDirection `json:"direction,omitempty"` + Status HostedAgentWorkspaceTransferStatus `json:"status"` + UploadID string `json:"upload_id,omitempty"` + PartSize int64 `json:"part_size,omitempty"` + ExpiresAt *Timestamp `json:"expires_at,omitempty"` + SizeBytes int64 `json:"size_bytes,omitempty"` + BytesWritten int64 `json:"bytes_written,omitempty"` + SHA256 string `json:"sha256,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +// HostedAgentWorkspaceTransferPartUploadURLsRequest requests presigned URLs for +// one or more upload parts. PartNumbers are 1-based. +type HostedAgentWorkspaceTransferPartUploadURLsRequest struct { + PartNumbers []int `json:"part_numbers"` +} + +// HostedAgentWorkspaceTransferPartUploadURL is one entry in a part-upload-urls response. +// PUT the part bytes directly to UploadURL (not through OHS). +type HostedAgentWorkspaceTransferPartUploadURL struct { + PartNumber int `json:"part_number"` + UploadURL string `json:"upload_url"` +} + +// HostedAgentWorkspaceTransferPartUploadURLs is returned by CreateWorkspaceTransferPartUploadURLs. +type HostedAgentWorkspaceTransferPartUploadURLs struct { + PartURLs []HostedAgentWorkspaceTransferPartUploadURL `json:"part_urls"` +} + +// HostedAgentWorkspaceTransferCommitRequest finalizes an upload after all parts are PUT. +type HostedAgentWorkspaceTransferCommitRequest struct { + SHA256 string `json:"sha256,omitempty"` +} + +// HostedAgentWorkspaceTransferCancelRequest aborts an in-flight transfer. +type HostedAgentWorkspaceTransferCancelRequest struct { + Reason string `json:"reason,omitempty"` +} + +// HostedAgentWorkspaceTransferCancelResponse is returned by CancelWorkspaceTransfer. +type HostedAgentWorkspaceTransferCancelResponse struct { + TransferID string `json:"transfer_id"` + Aborted bool `json:"aborted"` + Status HostedAgentWorkspaceTransferStatus `json:"status"` +} + +type hostedAgentSessionRoot struct { + Session *HostedAgentSession `json:"session"` +} + +// CreateSession provisions a new hosted agent session. +func (s *HostedAgentsServiceOp) CreateSession(ctx context.Context, create *HostedAgentSessionCreateRequest) (*HostedAgentSession, *Response, error) { + if create == nil { + return nil, nil, errors.New("hosted agents: create request is required") + } + if create.AgentKind == "" || create.AgentKind == HostedAgentKindUnspecified { + return nil, nil, errors.New("hosted agents: agent_kind is required") + } + req, err := s.client.NewRequest(ctx, http.MethodPost, hostedAgentsSessionsBasePath, create) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentSessionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Session == nil { + return nil, resp, errors.New("hosted agents: create session returned no session") + } + return root.Session, resp, nil +} + +func (s *HostedAgentsServiceOp) CreateSessionFromManifest(ctx context.Context, manifest []byte, opt *HostedAgentManifestCreateOptions) (*HostedAgentSession, *Response, error) { + if len(bytes.TrimSpace(manifest)) == 0 { + return nil, nil, errors.New("hosted agents: manifest is required") + } + path, err := addOptions(hostedAgentsSessionsBasePath, opt) + if err != nil { + return nil, nil, err + } + req, err := s.newCreateSessionPostRequest(ctx, path, bytes.NewReader(manifest), hostedAgentManifestMediaType) + if err != nil { + return nil, nil, err + } + return s.doCreateSession(ctx, req) +} + +// CreateSessionFromEnvironment provisions a session from a durable Environment. +// The JSON {name, config_id} body selects the environment-backed create path on +// the server, which reloads the Environment's stored manifest and resolves its +// declared credentials into the new session. +func (s *HostedAgentsServiceOp) CreateSessionFromEnvironment(ctx context.Context, create *HostedAgentSessionFromEnvironmentRequest) (*HostedAgentSession, *Response, error) { + return s.CreateSessionFromConfig(ctx, create) +} + +// CreateSessionFromConfig provisions a session from a durable Environment. +// Prefer CreateSessionFromEnvironment; this name remains for compatibility. +// The JSON {name, config_id} body selects the environment-backed create path on +// the server, which reloads the Environment's stored manifest and resolves its +// declared credentials into the new session. +func (s *HostedAgentsServiceOp) CreateSessionFromConfig(ctx context.Context, create *HostedAgentSessionFromConfigRequest) (*HostedAgentSession, *Response, error) { + if create == nil { + return nil, nil, errors.New("hosted agents: create request is required") + } + if create.Name == "" { + return nil, nil, errors.New("hosted agents: name is required") + } + if create.ConfigID == "" { + return nil, nil, errors.New("hosted agents: config_id is required") + } + req, err := s.client.NewRequest(ctx, http.MethodPost, hostedAgentsSessionsBasePath, create) + if err != nil { + return nil, nil, err + } + return s.doCreateSession(ctx, req) +} + +func (s *HostedAgentsServiceOp) doCreateSession(ctx context.Context, req *http.Request) (*HostedAgentSession, *Response, error) { + root := new(hostedAgentSessionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Session == nil { + return nil, resp, errors.New("hosted agents: create session returned no session") + } + return root.Session, resp, nil +} + +func (s *HostedAgentsServiceOp) newCreateSessionPostRequest(ctx context.Context, path string, body io.Reader, contentType string) (*http.Request, error) { + u, err := s.client.BaseURL.Parse(path) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept", mediaType) + req.Header.Set("User-Agent", s.client.UserAgent) + return req, nil +} + +// ListSessions returns sessions visible to the caller's team. +// +// The server omits simulation and evaluation sessions from the list so customer +// surfaces stay free of product-owned internal runs. GetSession by session_id +// still returns those sessions for the owning product workflow. +func (s *HostedAgentsServiceOp) ListSessions(ctx context.Context, opt *HostedAgentSessionListOptions) (*HostedAgentSessionsListResponse, *Response, error) { + path, err := addOptions(hostedAgentsSessionsBasePath, opt) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentSessionsListResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// GetSession returns a single session by ID. +func (s *HostedAgentsServiceOp) GetSession(ctx context.Context, sessionID string) (*HostedAgentSession, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + path := fmt.Sprintf(hostedAgentSessionByIDPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentSessionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Session == nil { + return nil, resp, errors.New("hosted agents: get session returned no session") + } + return root.Session, resp, nil +} + +// UpdateSession patches session-scoped settings and returns the updated +// session. Only the fields set on update are sent; the rest are left alone. +// A session in a terminal state (destroyed, failed) cannot be patched and the +// API answers 409. +func (s *HostedAgentsServiceOp) UpdateSession(ctx context.Context, sessionID string, update *HostedAgentSessionUpdateRequest) (*HostedAgentSession, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if update.empty() { + return nil, nil, errors.New("hosted agents: update request must set at least one field") + } + path := fmt.Sprintf(hostedAgentSessionByIDPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPatch, path, update) + if err != nil { + return nil, nil, err + } + root := new(hostedAgentSessionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if root.Session == nil { + return nil, resp, errors.New("hosted agents: update session returned no session") + } + return root.Session, resp, nil +} + +// DestroySession tears down a session. The API returns HTTP 204 on success. +func (s *HostedAgentsServiceOp) DestroySession(ctx context.Context, sessionID string) (*Response, error) { + if sessionID == "" { + return nil, errors.New("hosted agents: session id is required") + } + path := fmt.Sprintf(hostedAgentSessionByIDPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +func (s *HostedAgentsServiceOp) PauseSession(ctx context.Context, sessionID string) (*Response, error) { + if sessionID == "" { + return nil, errors.New("hosted agents: session id is required") + } + path := fmt.Sprintf(hostedAgentSessionPausePath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, struct{}{}) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +func (s *HostedAgentsServiceOp) ResumeSession(ctx context.Context, sessionID string) (*Response, error) { + if sessionID == "" { + return nil, errors.New("hosted agents: session id is required") + } + path := fmt.Sprintf(hostedAgentSessionResumePath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, struct{}{}) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// StreamSession opens the SSE stream for a session. Callers MUST Close the stream. +// +// Both reads are served by the data plane at GET .../sessions/{id}/events; the +// two modes differ in where the stream starts and whether it ends: +// +// - Live (the default) delivers forward-only from the moment of attach, +// preceded by whatever recent history the server decides to replay, and +// holds the connection open. ReplayFrom is sent as the standard +// Last-Event-ID header. +// - ReplayOnly adds ?replay_only=true: the server writes the session's stored +// event history and then ends the stream, so the read terminates on its own. +// ReplayFrom is sent as the replay_from query parameter, since it is an +// explicit pagination cursor here rather than a resume hint. Before and +// Limit page backwards from an event id within this mode; see +// HostedAgentSessionStreamOptions.Before and HasMore. +// +// Both carry HostedAgentEventKindStreamState control frames (see +// HostedAgentStreamState). A replay-only read reports catching_up and then +// simply ends; it never reaches live. +func (s *HostedAgentsServiceOp) StreamSession(ctx context.Context, sessionID string, opt *HostedAgentSessionStreamOptions) (*HostedAgentSessionStream, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + replayOnly := opt != nil && opt.ReplayOnly + cursor := "" + before := "" + limit := 0 + includeRaw := false + if opt != nil { + // The server answers this combination with a 400; rejecting it here + // spends no round trip to learn the same thing. + if opt.Before != "" && !opt.ReplayOnly { + return nil, nil, errors.New("hosted agents: before requires replay only") + } + cursor = opt.ReplayFrom + before = opt.Before + limit = opt.Limit + includeRaw = opt.IncludeRaw + } + + path := fmt.Sprintf(hostedAgentSessionEventsPath, sessionID) + q := url.Values{} + if replayOnly { + q.Set("replay_only", "true") + if cursor != "" { + q.Set("replay_from", cursor) + } + if before != "" { + q.Set("before", before) + } + if limit > 0 { + q.Set("limit", strconv.Itoa(limit)) + } + } + if includeRaw { + q.Set("include_raw", "true") + } + if encoded := q.Encode(); encoded != "" { + path += "?" + encoded + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Connection", "keep-alive") + if !replayOnly && cursor != "" { + req.Header.Set(sseLastEventIDHeader, cursor) + } + + resp, err := s.client.DoStream(ctx, req) + if err != nil { + return nil, resp, err + } + stream := &HostedAgentSessionStream{ + raw: NewSSEReader(resp.Body), + body: resp.Body, + } + stream.raw.OnComment = stream.observeComment + return stream, resp, nil +} + +// SendInput enqueues user text for the in-sandbox agent runtime. +func (s *HostedAgentsServiceOp) SendInput(ctx context.Context, sessionID string, input *HostedAgentSendInputRequest) (*HostedAgentSendInputResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if input == nil { + return nil, nil, errors.New("hosted agents: input is required") + } + if strings.TrimSpace(input.Text) == "" { + return nil, nil, errors.New("hosted agents: text is required") + } + path := fmt.Sprintf(hostedAgentSessionInputPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, input) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentSendInputResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// RelayRequest forwards one native agent-protocol request frame to the +// session's agent and returns its reply verbatim. Blocks on the agent, so it +// is slower than the other session calls; an empty reply means the in-sandbox +// adapter declined the method. +func (s *HostedAgentsServiceOp) RelayRequest(ctx context.Context, sessionID string, body *HostedAgentRelayRequest) (*HostedAgentRelayResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if body == nil || len(body.SourceRaw) == 0 { + return nil, nil, errors.New("hosted agents: source_raw is required") + } + path := fmt.Sprintf(hostedAgentSessionRequestPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, body) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentRelayResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ResolveHITL submits a human-in-the-loop decision. The API returns HTTP 204 on success. +func (s *HostedAgentsServiceOp) ResolveHITL(ctx context.Context, sessionID, requestID string, body *HostedAgentResolveHITLRequest) (*Response, error) { + if sessionID == "" { + return nil, errors.New("hosted agents: session id is required") + } + if requestID == "" { + return nil, errors.New("hosted agents: request id is required") + } + if body == nil { + return nil, errors.New("hosted agents: resolve request is required") + } + if body.Outcome == "" || body.Outcome == HostedAgentHITLOutcomeUnspecified { + return nil, errors.New("hosted agents: outcome is required") + } + path := fmt.Sprintf(hostedAgentSessionHITLPath, sessionID, requestID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, body) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// StartProviderAuth begins (or resumes) the team-scoped connect flow for an +// external provider. The server derives the team from the authenticated +// principal, so the POST carries an empty JSON object body. +func (s *HostedAgentsServiceOp) StartProviderAuth(ctx context.Context, provider string) (*HostedAgentProviderAuthStart, *Response, error) { + if provider == "" { + return nil, nil, errors.New("hosted agents: provider is required") + } + path := fmt.Sprintf(hostedAgentProviderAuthPath, provider) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, struct{}{}) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentProviderAuthStart) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// PollProviderAuth checks whether a pending connect link has been authorized. +// pollURL is the PollURL returned by StartProviderAuth; it is forwarded to the +// server as the poll_url query parameter. +func (s *HostedAgentsServiceOp) PollProviderAuth(ctx context.Context, provider, pollURL string) (*HostedAgentProviderAuthPoll, *Response, error) { + if provider == "" { + return nil, nil, errors.New("hosted agents: provider is required") + } + if pollURL == "" { + return nil, nil, errors.New("hosted agents: poll url is required") + } + path := fmt.Sprintf(hostedAgentProviderAuthPollPath, provider) + q := url.Values{} + q.Set("poll_url", pollURL) + path += "?" + q.Encode() + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentProviderAuthPoll) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ExecInSandbox runs a command inside the session sandbox. +func (s *HostedAgentsServiceOp) ExecInSandbox(ctx context.Context, sessionID string, body *HostedAgentSandboxExecRequest) (*HostedAgentSandboxExecResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if body == nil { + return nil, nil, errors.New("hosted agents: exec request is required") + } + if len(body.Argv) == 0 { + return nil, nil, errors.New("hosted agents: argv is required") + } + path := fmt.Sprintf(hostedAgentSessionSandboxExecPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, body) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentSandboxExecResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// ListSandboxSizes returns the customer-selectable sandbox (microVM) size +// catalog. The set is the single source of truth shared with create-time +// validation; every returned slug is accepted as spec.sandbox.sizeSlug. +// Sizes are ordered smallest-to-largest. The catalog is static and independent +// of team/session; there is no pagination. +func (s *HostedAgentsServiceOp) ListSandboxSizes(ctx context.Context) (*HostedAgentSandboxSizesResponse, *Response, error) { + req, err := s.client.NewRequest(ctx, http.MethodGet, hostedAgentSandboxSizesPath, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentSandboxSizesResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// UploadWorkspace streams a file (or tar archive) into the session workspace. +func (s *HostedAgentsServiceOp) UploadWorkspace(ctx context.Context, sessionID string, input *HostedAgentWorkspaceUploadRequest) (*HostedAgentWorkspaceUploadResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if input == nil { + return nil, nil, errors.New("hosted agents: upload request is required") + } + if input.Path == "" { + return nil, nil, errors.New("hosted agents: path is required") + } + if input.Body == nil { + return nil, nil, errors.New("hosted agents: body is required") + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceUploadPath, sessionID) + u, err := s.client.BaseURL.Parse(path) + if err != nil { + return nil, nil, err + } + q := url.Values{} + q.Set("path", input.Path) + if input.IsArchive { + q.Set("is_archive", "true") + } + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), input.Body) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", hostedAgentWorkspaceMediaType) + req.Header.Set("Accept", mediaType) + req.Header.Set("User-Agent", s.client.UserAgent) + if input.ContentSHA256 != "" { + req.Header.Set(workspaceContentSHA256Header, input.ContentSHA256) + } + + root := new(HostedAgentWorkspaceUploadResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// DownloadWorkspace streams a file (or tar archive) out of the session +// workspace. Callers MUST read the returned Body to EOF and then Close it; +// the body strips the trailing integrity footer and verifies SHA-256 of the +// payload. +func (s *HostedAgentsServiceOp) DownloadWorkspace(ctx context.Context, sessionID string, input *HostedAgentWorkspaceDownloadRequest) (*HostedAgentWorkspaceDownload, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if input == nil { + return nil, nil, errors.New("hosted agents: download request is required") + } + if input.Path == "" { + return nil, nil, errors.New("hosted agents: path is required") + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceDownloadPath, sessionID) + q := url.Values{} + q.Set("path", input.Path) + if input.AsArchive { + q.Set("as_archive", "true") + } + path += "?" + q.Encode() + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", hostedAgentWorkspaceMediaType) + + resp, err := s.client.DoStream(ctx, req) + if err != nil { + return nil, resp, err + } + + out := &HostedAgentWorkspaceDownload{ + Body: &workspaceDownloadBody{ + body: resp.Body, + hasher: sha256.New(), + }, + } + if archive, perr := strconv.ParseBool(strings.TrimSpace(resp.Header.Get(workspaceIsArchiveHeader))); perr == nil { + out.IsArchive = archive + } + if hint := strings.TrimSpace(resp.Header.Get(workspaceSizeBytesHeader)); hint != "" { + if n, perr := strconv.ParseInt(hint, 10, 64); perr == nil { + out.SizeBytes = n + } + } + return out, resp, nil +} + +// CreateWorkspaceTransfer starts a large-file staged upload or download. +// For uploads, response includes part_size for client-side chunking. +func (s *HostedAgentsServiceOp) CreateWorkspaceTransfer(ctx context.Context, sessionID string, input *HostedAgentWorkspaceTransferCreateRequest) (*HostedAgentWorkspaceTransfer, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if input == nil { + return nil, nil, errors.New("hosted agents: transfer create request is required") + } + if input.Direction == "" { + return nil, nil, errors.New("hosted agents: direction is required") + } + if input.Path == "" { + return nil, nil, errors.New("hosted agents: path is required") + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceTransfersPath, sessionID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, input) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentWorkspaceTransfer) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// CreateWorkspaceTransferPartUploadURLs returns presigned URLs to PUT one or more +// upload parts. Upload only. Part numbers start at 1. If a URL expires, request +// that part_number again. +func (s *HostedAgentsServiceOp) CreateWorkspaceTransferPartUploadURLs(ctx context.Context, sessionID, transferID string, input *HostedAgentWorkspaceTransferPartUploadURLsRequest) (*HostedAgentWorkspaceTransferPartUploadURLs, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if transferID == "" { + return nil, nil, errors.New("hosted agents: transfer id is required") + } + if input == nil { + return nil, nil, errors.New("hosted agents: part upload URLs request is required") + } + if len(input.PartNumbers) == 0 { + return nil, nil, errors.New("hosted agents: part_numbers must not be empty") + } + for _, n := range input.PartNumbers { + if n < 1 { + return nil, nil, errors.New("hosted agents: part_numbers must all be >= 1") + } + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceTransferPartURLsPath, sessionID, transferID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, input) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentWorkspaceTransferPartUploadURLs) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// CommitWorkspaceTransfer finalizes an upload after all parts have been PUT and +// starts applying the file into the workspace. Poll GetWorkspaceTransfer afterward. +func (s *HostedAgentsServiceOp) CommitWorkspaceTransfer(ctx context.Context, sessionID, transferID string, input *HostedAgentWorkspaceTransferCommitRequest) (*HostedAgentWorkspaceTransfer, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if transferID == "" { + return nil, nil, errors.New("hosted agents: transfer id is required") + } + if input == nil { + input = &HostedAgentWorkspaceTransferCommitRequest{} + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceTransferCommitPath, sessionID, transferID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, input) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentWorkspaceTransfer) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// GetWorkspaceTransfer polls transfer status. On a completed download, DownloadURL +// and SHA256 are set; verify SHA256(file) == SHA256 after fetching DownloadURL. +// This path has no DOWSSHA1 body footer. +func (s *HostedAgentsServiceOp) GetWorkspaceTransfer(ctx context.Context, sessionID, transferID string) (*HostedAgentWorkspaceTransfer, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if transferID == "" { + return nil, nil, errors.New("hosted agents: transfer id is required") + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceTransferByIDPath, sessionID, transferID) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentWorkspaceTransfer) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// CancelWorkspaceTransfer aborts an in-flight transfer. Idempotent. +func (s *HostedAgentsServiceOp) CancelWorkspaceTransfer(ctx context.Context, sessionID, transferID string, input *HostedAgentWorkspaceTransferCancelRequest) (*HostedAgentWorkspaceTransferCancelResponse, *Response, error) { + if sessionID == "" { + return nil, nil, errors.New("hosted agents: session id is required") + } + if transferID == "" { + return nil, nil, errors.New("hosted agents: transfer id is required") + } + if input == nil { + input = &HostedAgentWorkspaceTransferCancelRequest{} + } + + path := fmt.Sprintf(hostedAgentSessionWorkspaceTransferCancelPath, sessionID, transferID) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, input) + if err != nil { + return nil, nil, err + } + root := new(HostedAgentWorkspaceTransferCancelResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root, resp, nil +} + +// workspaceDownloadBody holds back the trailing integrity footer while +// streaming, hashes the payload, and verifies the footer at EOF. +type workspaceDownloadBody struct { + body io.ReadCloser + hasher hash.Hash + pending []byte + scratch []byte + sawEOF bool + verified bool + verr error +} + +func (b *workspaceDownloadBody) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if b.verified { + if b.verr != nil { + return 0, b.verr + } + return 0, io.EOF + } + + for { + if overflow := len(b.pending) - workspaceDownloadFooterLen; overflow > 0 { + n := overflow + if n > len(p) { + n = len(p) + } + copy(p, b.pending[:n]) + b.hasher.Write(b.pending[:n]) + b.pending = b.pending[n:] + return n, nil + } + + if b.sawEOF { + if err := b.verifyFooter(); err != nil { + return 0, err + } + return 0, io.EOF + } + + if b.scratch == nil { + b.scratch = make([]byte, 32*1024) + } + nr, err := b.body.Read(b.scratch) + if nr > 0 { + b.pending = append(b.pending, b.scratch[:nr]...) + } + if errors.Is(err, io.EOF) { + b.sawEOF = true + continue + } + if err != nil { + return 0, err + } + } +} + +func (b *workspaceDownloadBody) verifyFooter() error { + if b.verified { + return b.verr + } + b.verified = true + + if len(b.pending) != workspaceDownloadFooterLen { + b.verr = errors.New("hosted agents: missing or truncated workspace download integrity footer") + return b.verr + } + want, ok := parseWorkspaceDownloadFooter(b.pending) + if !ok { + b.verr = errors.New("hosted agents: invalid workspace download integrity footer") + return b.verr + } + got := hex.EncodeToString(b.hasher.Sum(nil)) + if !strings.EqualFold(want, got) { + b.verr = fmt.Errorf("hosted agents: workspace download checksum mismatch: want %q, got %q", want, got) + return b.verr + } + b.pending = nil + return nil +} + +// parseWorkspaceDownloadFooter validates DOWSSHA1<64-hex>\n and returns the digest. +func parseWorkspaceDownloadFooter(footer []byte) (string, bool) { + if len(footer) != workspaceDownloadFooterLen { + return "", false + } + if string(footer[:len(workspaceDownloadFooterPrefix)]) != workspaceDownloadFooterPrefix { + return "", false + } + if footer[workspaceDownloadFooterLen-1] != '\n' { + return "", false + } + digest := string(footer[len(workspaceDownloadFooterPrefix) : workspaceDownloadFooterLen-1]) + for i := 0; i < len(digest); i++ { + c := digest[i] + switch { + case c >= '0' && c <= '9': + case c >= 'a' && c <= 'f': + case c >= 'A' && c <= 'F': + default: + return "", false + } + } + return digest, true +} + +func (b *workspaceDownloadBody) Close() error { + if cerr := b.body.Close(); cerr != nil { + return cerr + } + if b.verified { + return b.verr + } + return nil +} + +// HostedAgentSessionStream is a typed iterator over a session SSE stream. +type HostedAgentSessionStream struct { + raw *SSEReader + body io.ReadCloser + current HostedAgentEvent + err error + done bool + hasMore bool +} + +// hasMoreComment is the SSE comment a history page (see +// HostedAgentSessionStreamOptions.Before) ends with, reporting whether events +// older than the page remain. +const hasMoreComment = "has_more=" + +// observeComment records the has_more trailer of a history page. Wired as the +// reader's OnComment hook, so it runs while Next drains the stream. +func (s *HostedAgentSessionStream) observeComment(comment []byte) { + value, ok := strings.CutPrefix(string(comment), hasMoreComment) + if !ok { + return + } + s.hasMore = value == "true" +} + +// HasMore reports whether the history page just read has older events behind +// it. Only meaningful once a stream opened with +// HostedAgentSessionStreamOptions.Before has been drained to its end: the +// trailer arrives after the last event. +func (s *HostedAgentSessionStream) HasMore() bool { return s.hasMore } + +// Next advances to the next event. Returns false on EOF or error. +func (s *HostedAgentSessionStream) Next() bool { + for { + if s.done || s.err != nil { + return false + } + ev, err := s.raw.Next() + if errors.Is(err, io.EOF) { + s.done = true + return false + } + if err != nil { + s.err = err + return false + } + if len(ev.Data) == 0 { + continue + } + var event HostedAgentEvent + if err := json.Unmarshal(ev.Data, &event); err != nil { + s.err = err + return false + } + if event.EventID == "" && ev.ID != "" { + event.EventID = ev.ID + } + if event.Kind == "" && ev.Event != "" { + event.Kind = HostedAgentEventKind(ev.Event) + } + s.current = event + return true + } +} + +// Current returns the most recent event produced by Next. +func (s *HostedAgentSessionStream) Current() HostedAgentEvent { return s.current } + +// Err returns any non-EOF error encountered during iteration. +func (s *HostedAgentSessionStream) Err() error { return s.err } + +// Close releases the underlying HTTP response body. Always call Close. +func (s *HostedAgentSessionStream) Close() error { + if s.body == nil { + return nil + } + return s.body.Close() +} diff --git a/vendor/github.com/digitalocean/godo/kubernetes.go b/vendor/github.com/digitalocean/godo/kubernetes.go index 609ffd06c..fe6a6fa9d 100644 --- a/vendor/github.com/digitalocean/godo/kubernetes.go +++ b/vendor/github.com/digitalocean/godo/kubernetes.go @@ -98,6 +98,7 @@ type KubernetesClusterCreateRequest struct { CorednsAutoscaler *KubernetesCorednsAutoscaler `json:"coredns_autoscaler,omitempty"` SSO *KubernetesClusterSSO `json:"sso,omitempty"` P2pOciRegistryPlugin *KubernetesP2pOciRegistry `json:"p2p_oci_registry_plugin,omitempty"` + NfsCsiPlugin *KubernetesNfsCsiPlugin `json:"nfs_csi_plugin,omitempty"` // IsolatedWorkers enables isolated worker nodes. When true, the cluster's VPC // must already have a NAT gateway attached, or the create request fails with a // 422. This can only be set at creation time. @@ -123,6 +124,7 @@ type KubernetesClusterUpdateRequest struct { CorednsAutoscaler *KubernetesCorednsAutoscaler `json:"coredns_autoscaler,omitempty"` SSO *KubernetesClusterSSO `json:"sso,omitempty"` P2pOciRegistryPlugin *KubernetesP2pOciRegistry `json:"p2p_oci_registry_plugin,omitempty"` + NfsCsiPlugin *KubernetesNfsCsiPlugin `json:"nfs_csi_plugin,omitempty"` // Convert cluster to run highly available control plane HA *bool `json:"ha,omitempty"` @@ -279,6 +281,7 @@ type KubernetesCluster struct { CorednsAutoscaler *KubernetesCorednsAutoscaler `json:"coredns_autoscaler,omitempty"` SSO *KubernetesClusterSSO `json:"sso,omitempty"` P2pOciRegistryPlugin *KubernetesP2pOciRegistry `json:"p2p_oci_registry_plugin,omitempty"` + NfsCsiPlugin *KubernetesNfsCsiPlugin `json:"nfs_csi_plugin,omitempty"` IsolatedWorkers bool `json:"isolated_workers,omitempty"` Status *KubernetesClusterStatus `json:"status,omitempty"` @@ -379,6 +382,11 @@ type KubernetesP2pOciRegistry struct { Enabled *bool `json:"enabled"` } +// KubernetesNfsCsiPlugin represents information about the NFS CSI driver cluster plugin. +type KubernetesNfsCsiPlugin struct { + Enabled *bool `json:"enabled"` +} + // KubernetesClusterSSO configures Single Sign-On (SSO) for a Kubernetes cluster. type KubernetesClusterSSO struct { Enabled bool `json:"enabled"` diff --git a/vendor/github.com/digitalocean/godo/micro_droplets.go b/vendor/github.com/digitalocean/godo/micro_droplets.go deleted file mode 100644 index d3a364ce4..000000000 --- a/vendor/github.com/digitalocean/godo/micro_droplets.go +++ /dev/null @@ -1,551 +0,0 @@ -package godo - -import ( - "context" - "fmt" - "net/http" -) - -const ( - microDropletBasePath = "v2/microdroplets" - microDropletCheckpointsBasePath = "v2/microdroplets/checkpoints" - microDropletOptionsPath = "v2/microdroplets/options" -) - -// MicroDropletState represents the lifecycle state of a MicroDroplet. -type MicroDropletState string - -// Possible lifecycle states for a MicroDroplet. -const ( - MicroDropletStateUnknown = MicroDropletState("unknown") - MicroDropletStateCreating = MicroDropletState("creating") - MicroDropletStateRunning = MicroDropletState("running") - MicroDropletStatePausing = MicroDropletState("pausing") - MicroDropletStatePaused = MicroDropletState("paused") - MicroDropletStateResuming = MicroDropletState("resuming") - MicroDropletStateTerminating = MicroDropletState("terminating") - MicroDropletStateTerminated = MicroDropletState("terminated") - MicroDropletStateFailed = MicroDropletState("failed") -) - -// MicroDropletNetworking represents the networking mode of a MicroDroplet. -type MicroDropletNetworking string - -// Possible networking modes for a MicroDroplet. -const ( - MicroDropletNetworkingUnknown = MicroDropletNetworking("unknown") - MicroDropletNetworkingPublic = MicroDropletNetworking("public") - MicroDropletNetworkingVPC = MicroDropletNetworking("vpc") -) - -// MicroDropletHTTPProtocol represents the HTTP protocol option for a MicroDroplet. -type MicroDropletHTTPProtocol string - -// Possible HTTP protocol values for a MicroDroplet. -const ( - MicroDropletHTTPProtocolHTTP = MicroDropletHTTPProtocol("http") - MicroDropletHTTPProtocolHTTP2 = MicroDropletHTTPProtocol("http2") -) - -// MicroDropletURLStatus represents the lifecycle of a MicroDroplet URL. -type MicroDropletURLStatus string - -// Possible statuses for a MicroDroplet URL. -const ( - MicroDropletURLStatusPending = MicroDropletURLStatus("PENDING") - MicroDropletURLStatusActive = MicroDropletURLStatus("ACTIVE") -) - -// MicroDropletCheckpointStatus represents the status of a MicroDroplet checkpoint. -type MicroDropletCheckpointStatus string - -// Possible states for a MicroDroplet checkpoint. -const ( - MicroDropletCheckpointStatusUnknown = MicroDropletCheckpointStatus("CHECKPOINT_UNKNOWN") - MicroDropletCheckpointStatusCreating = MicroDropletCheckpointStatus("CHECKPOINT_CREATING") - MicroDropletCheckpointStatusAvailable = MicroDropletCheckpointStatus("CHECKPOINT_AVAILABLE") - MicroDropletCheckpointStatusFailed = MicroDropletCheckpointStatus("CHECKPOINT_FAILED") - MicroDropletCheckpointStatusDeleted = MicroDropletCheckpointStatus("CHECKPOINT_DELETED") - MicroDropletCheckpointStatusDeleting = MicroDropletCheckpointStatus("CHECKPOINT_DELETING") -) - -// MicroDropletsService is an interface for interfacing with the MicroDroplet -// endpoints of the DigitalOcean API. -// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/MicroDroplets -type MicroDropletsService interface { - List(ctx context.Context, opt *ListOptions) ([]MicroDroplet, *Response, error) - ListByRegion(ctx context.Context, region string, opt *ListOptions) ([]MicroDroplet, *Response, error) - ListByName(ctx context.Context, name string, opt *ListOptions) ([]MicroDroplet, *Response, error) - Get(ctx context.Context, id string) (*MicroDroplet, *Response, error) - Create(ctx context.Context, createRequest *MicroDropletCreateRequest) (*MicroDroplet, *Response, error) - Pause(ctx context.Context, id string) (*MicroDroplet, *Response, error) - Resume(ctx context.Context, id string) (*MicroDroplet, *Response, error) - Delete(ctx context.Context, id string) (*Response, error) - - ListCheckpoints(ctx context.Context, opt *ListMicroDropletCheckpointsOptions) ([]MicroDropletCheckpoint, *Response, error) - CreateCheckpoint(ctx context.Context, microDropletID string, createRequest *MicroDropletCheckpointCreateRequest) (*MicroDropletCheckpoint, *Response, error) - GetCheckpoint(ctx context.Context, id string) (*MicroDropletCheckpoint, *Response, error) - DeleteCheckpoint(ctx context.Context, id string) (*Response, error) - - GetCreateOptions(ctx context.Context) (*MicroDropletCreateOptions, *Response, error) -} - -// MicroDropletsServiceOp handles communication with the MicroDroplet related -// methods of the DigitalOcean API. -type MicroDropletsServiceOp struct { - client *Client -} - -var _ MicroDropletsService = &MicroDropletsServiceOp{} - -// MicroDropletSize is the compute capacity reported on a MicroDroplet. -type MicroDropletSize struct { - CPU uint32 `json:"cpu,omitempty"` - Memory uint32 `json:"memory,omitempty"` - Disk uint64 `json:"disk,omitempty"` -} - -// MicroDropletSizeRequest is the compute capacity requested at create time. -// Disk is provisioned with the size and is not requested. -type MicroDropletSizeRequest struct { - CPU uint32 `json:"cpu"` - Memory uint32 `json:"memory"` -} - -// MicroDropletSource names what a MicroDroplet runs. Exactly one of OCIRef or -// CheckpointID must be set on create; the API rejects both or neither. -type MicroDropletSource struct { - OCIRef string `json:"oci_ref,omitempty"` - CheckpointID string `json:"checkpoint_id,omitempty"` -} - -// MicroDropletURL is one ingress URL attached to a MicroDroplet. -type MicroDropletURL struct { - Hostname string `json:"hostname,omitempty"` - Port int `json:"port,omitempty"` - Default bool `json:"default,omitempty"` - Status MicroDropletURLStatus `json:"status,omitempty"` -} - -// MicroDroplet represents a DigitalOcean MicroDroplet. -type MicroDroplet struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Region string `json:"region,omitempty"` - State MicroDropletState `json:"state,omitempty"` - Size *MicroDropletSize `json:"size,omitempty"` - URLs []MicroDropletURL `json:"urls,omitempty"` - Ports []uint32 `json:"ports,omitempty"` - FailureReason string `json:"failure_reason,omitempty"` - Networking MicroDropletNetworking `json:"networking,omitempty"` - Source *MicroDropletSource `json:"source,omitempty"` - AutoPause *AutoPauseConfig `json:"auto_pause,omitempty"` - AutoResume *bool `json:"auto_resume,omitempty"` - Created string `json:"created_at,omitempty"` - Tags []string `json:"tags,omitempty"` -} - -// AutoPauseConfig configures MicroDroplet auto-pause behavior. IdleTimeout is -// a Go duration string (e.g. "5m", "30s") describing how long the MicroDroplet -// must be idle before it is paused. -type AutoPauseConfig struct { - Enabled *bool `json:"enabled,omitempty"` - IdleTimeout string `json:"idle_timeout,omitempty"` -} - -// MicroDropletCheckpoint represents a checkpoint of a MicroDroplet -// (persisted memory + disk state). -type MicroDropletCheckpoint struct { - ID string `json:"id,omitempty"` - MicroDropletID string `json:"micro_droplet_id,omitempty"` - MicroDropletName string `json:"micro_droplet_name,omitempty"` - Name string `json:"name,omitempty"` - Region string `json:"region,omitempty"` - Status MicroDropletCheckpointStatus `json:"status,omitempty"` - MemoryBytes uint64 `json:"memory_bytes,omitempty"` - DiskBytes uint64 `json:"disk_bytes,omitempty"` - Created string `json:"created_at,omitempty"` -} - -// MicroDropletCreateRequest represents a request to create a MicroDroplet. -// Size and Region may be omitted when restoring from a checkpoint so the API -// can inherit them from the checkpoint (api-v2 C6). -type MicroDropletCreateRequest struct { - Name string `json:"name"` - Region string `json:"region,omitempty"` - Size *MicroDropletSizeRequest `json:"size,omitempty"` - Source *MicroDropletSource `json:"source"` - Networking MicroDropletNetworking `json:"networking,omitempty"` - VPCUUID string `json:"vpc_uuid,omitempty"` - AutoPause *AutoPauseConfig `json:"auto_pause,omitempty"` - AutoResume *bool `json:"auto_resume,omitempty"` - HTTPPort uint32 `json:"http_port,omitempty"` - HTTPProtocol MicroDropletHTTPProtocol `json:"http_protocol,omitempty"` - Ports []uint32 `json:"ports,omitempty"` - Environment map[string]string `json:"environment,omitempty"` - Tags []string `json:"tags,omitempty"` -} - -// MicroDropletCheckpointCreateRequest represents a request to create a checkpoint. -type MicroDropletCheckpointCreateRequest struct { - Name string `json:"name,omitempty"` -} - -// ListMicroDropletCheckpointsOptions are the optional parameters for listing -// checkpoints. MicroDropletID filters to checkpoints captured from that -// MicroDroplet; omit it to list all checkpoints for the team. -type ListMicroDropletCheckpointsOptions struct { - ListOptions - MicroDropletID string `url:"micro_droplet_id,omitempty"` -} - -// MicroDropletCreateOptions is the response from GET /v2/microdroplets/options. -type MicroDropletCreateOptions struct { - Regions []MicroDropletRegionOption `json:"regions,omitempty"` - DefaultRegion string `json:"default_region,omitempty"` - Sizes []MicroDropletSizeOption `json:"sizes,omitempty"` - Features []MicroDropletFeatureOption `json:"features,omitempty"` - AccountLimits *MicroDropletAccountLimits `json:"account_limits,omitempty"` -} - -// MicroDropletRegionOption is one region evaluated for the authenticated team. -type MicroDropletRegionOption struct { - Slug string `json:"slug,omitempty"` - Available bool `json:"available"` - UnavailableReason string `json:"unavailable_reason,omitempty"` -} - -// MicroDropletSizeOption is one supported size evaluated for the team. -type MicroDropletSizeOption struct { - Size MicroDropletSize `json:"size"` - Available bool `json:"available"` - Pricing *MicroDropletSizePricing `json:"pricing,omitempty"` -} - -// MicroDropletSizePricing is the USD price of one size. -type MicroDropletSizePricing struct { - PricePerHour float64 `json:"price_per_hour"` - PricePerMonth float64 `json:"price_per_month"` -} - -// MicroDropletFeatureOption is one product feature gate for the team. -type MicroDropletFeatureOption struct { - Name string `json:"name,omitempty"` - Enabled bool `json:"enabled"` -} - -// MicroDropletAccountLimits are the team's effective MicroDroplet limits. -type MicroDropletAccountLimits struct { - MaxConcurrentRunning uint64 `json:"max_concurrent_running,omitempty"` - MaxTotalCount uint64 `json:"max_total_count,omitempty"` - MaxMemoryBytes uint64 `json:"max_memory_bytes,omitempty"` - MaxDiskBytes uint64 `json:"max_disk_bytes,omitempty"` - MaxIdleTimeoutSeconds uint64 `json:"max_idle_timeout_seconds,omitempty"` -} - -// String returns a human-readable description of a MicroDroplet. -func (m MicroDroplet) String() string { - return Stringify(m) -} - -// URN returns the MicroDroplet ID in a valid DO API URN form. -func (m MicroDroplet) URN() string { - return ToURN("MicroDroplet", m.ID) -} - -// String returns a human-readable description of a MicroDropletCheckpoint. -func (c MicroDropletCheckpoint) String() string { - return Stringify(c) -} - -// String returns a human-readable description of a MicroDropletCreateRequest. -func (r MicroDropletCreateRequest) String() string { - return Stringify(r) -} - -type microDropletRoot struct { - MicroDroplet *MicroDroplet `json:"micro_droplet"` -} - -type microDropletsRoot struct { - MicroDroplets []MicroDroplet `json:"micro_droplets"` - Links *Links `json:"links"` - Meta *Meta `json:"meta"` -} - -type microDropletCheckpointRoot struct { - Checkpoint *MicroDropletCheckpoint `json:"checkpoint"` -} - -type microDropletCheckpointsRoot struct { - Checkpoints []MicroDropletCheckpoint `json:"checkpoints"` - Links *Links `json:"links"` - Meta *Meta `json:"meta"` -} - -// listMicroDropletOptions holds MicroDroplet-specific list filters that are -// not part of the shared ListOptions. -type listMicroDropletOptions struct { - Region string `url:"region,omitempty"` - Name string `url:"name,omitempty"` -} - -// List lists all MicroDroplets, with optional pagination. -func (s *MicroDropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]MicroDroplet, *Response, error) { - return s.list(ctx, opt, nil) -} - -// ListByRegion lists MicroDroplets filtered by region slug, with optional pagination. -func (s *MicroDropletsServiceOp) ListByRegion(ctx context.Context, region string, opt *ListOptions) ([]MicroDroplet, *Response, error) { - if region == "" { - return nil, nil, NewArgError("region", "cannot be empty") - } - return s.list(ctx, opt, &listMicroDropletOptions{Region: region}) -} - -// ListByName lists MicroDroplets filtered by exact name match, with optional pagination. -func (s *MicroDropletsServiceOp) ListByName(ctx context.Context, name string, opt *ListOptions) ([]MicroDroplet, *Response, error) { - if name == "" { - return nil, nil, NewArgError("name", "cannot be empty") - } - return s.list(ctx, opt, &listMicroDropletOptions{Name: name}) -} - -func (s *MicroDropletsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listMicroDropletOptions) ([]MicroDroplet, *Response, error) { - path := microDropletBasePath - path, err := addOptions(path, opt) - if err != nil { - return nil, nil, err - } - path, err = addOptions(path, listOpt) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(microDropletsRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - if l := root.Links; l != nil { - resp.Links = l - } - if m := root.Meta; m != nil { - resp.Meta = m - } - - return root.MicroDroplets, resp, nil -} - -// Get retrieves a MicroDroplet by its ID. -func (s *MicroDropletsServiceOp) Get(ctx context.Context, id string) (*MicroDroplet, *Response, error) { - if id == "" { - return nil, nil, NewArgError("id", "cannot be empty") - } - - path := fmt.Sprintf("%s/%s", microDropletBasePath, id) - - req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(microDropletRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.MicroDroplet, resp, nil -} - -// Create provisions a new MicroDroplet with the provided configuration. -func (s *MicroDropletsServiceOp) Create(ctx context.Context, createRequest *MicroDropletCreateRequest) (*MicroDroplet, *Response, error) { - if createRequest == nil { - return nil, nil, NewArgError("createRequest", "cannot be nil") - } - - req, err := s.client.NewRequest(ctx, http.MethodPost, microDropletBasePath, createRequest) - if err != nil { - return nil, nil, err - } - - root := new(microDropletRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.MicroDroplet, resp, nil -} - -// Pause synchronously transitions a RUNNING MicroDroplet to PAUSED. It blocks -// until the platform has durably paused the MicroDroplet and returns the -// updated resource. The call is idempotent: pausing a MicroDroplet that is -// already PAUSED returns the current MicroDroplet with no side effects. -func (s *MicroDropletsServiceOp) Pause(ctx context.Context, id string) (*MicroDroplet, *Response, error) { - return s.doTransition(ctx, id, "pause") -} - -// Resume synchronously transitions a PAUSED MicroDroplet to RUNNING. It blocks -// until the platform has durably resumed the MicroDroplet and returns the -// updated resource. The call is idempotent: resuming a MicroDroplet that is -// already RUNNING returns the current MicroDroplet with no side effects. -func (s *MicroDropletsServiceOp) Resume(ctx context.Context, id string) (*MicroDroplet, *Response, error) { - return s.doTransition(ctx, id, "resume") -} - -// doTransition posts an empty body to a MicroDroplet transition sub-resource -// (e.g. /pause, /resume) and decodes the returned MicroDroplet. -func (s *MicroDropletsServiceOp) doTransition(ctx context.Context, id, action string) (*MicroDroplet, *Response, error) { - if id == "" { - return nil, nil, NewArgError("id", "cannot be empty") - } - - path := fmt.Sprintf("%s/%s/%s", microDropletBasePath, id, action) - - req, err := s.client.NewRequest(ctx, http.MethodPost, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(microDropletRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.MicroDroplet, resp, nil -} - -// Delete removes a MicroDroplet by its ID. The DigitalOcean API returns a 204 -// on success and does not include a response body. -func (s *MicroDropletsServiceOp) Delete(ctx context.Context, id string) (*Response, error) { - if id == "" { - return nil, NewArgError("id", "cannot be empty") - } - - path := fmt.Sprintf("%s/%s", microDropletBasePath, id) - - req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// ListCheckpoints lists checkpoints for the authenticated team. Pass a -// MicroDropletID on opt to filter to checkpoints captured from that MicroDroplet. -func (s *MicroDropletsServiceOp) ListCheckpoints(ctx context.Context, opt *ListMicroDropletCheckpointsOptions) ([]MicroDropletCheckpoint, *Response, error) { - path := microDropletCheckpointsBasePath - path, err := addOptions(path, opt) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(microDropletCheckpointsRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - if l := root.Links; l != nil { - resp.Links = l - } - if m := root.Meta; m != nil { - resp.Meta = m - } - - return root.Checkpoints, resp, nil -} - -// CreateCheckpoint starts an asynchronous checkpoint of a running MicroDroplet. -func (s *MicroDropletsServiceOp) CreateCheckpoint(ctx context.Context, microDropletID string, createRequest *MicroDropletCheckpointCreateRequest) (*MicroDropletCheckpoint, *Response, error) { - if microDropletID == "" { - return nil, nil, NewArgError("microDropletID", "cannot be empty") - } - if createRequest == nil { - createRequest = &MicroDropletCheckpointCreateRequest{} - } - - path := fmt.Sprintf("%s/%s/checkpoints", microDropletBasePath, microDropletID) - - req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) - if err != nil { - return nil, nil, err - } - - root := new(microDropletCheckpointRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.Checkpoint, resp, nil -} - -// GetCheckpoint retrieves a checkpoint by its ID. -func (s *MicroDropletsServiceOp) GetCheckpoint(ctx context.Context, id string) (*MicroDropletCheckpoint, *Response, error) { - if id == "" { - return nil, nil, NewArgError("id", "cannot be empty") - } - - path := fmt.Sprintf("%s/%s", microDropletCheckpointsBasePath, id) - - req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(microDropletCheckpointRoot) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.Checkpoint, resp, nil -} - -// DeleteCheckpoint releases the state stored by a checkpoint. Returns a 204 -// on success with no response body. -func (s *MicroDropletsServiceOp) DeleteCheckpoint(ctx context.Context, id string) (*Response, error) { - if id == "" { - return nil, NewArgError("id", "cannot be empty") - } - - path := fmt.Sprintf("%s/%s", microDropletCheckpointsBasePath, id) - - req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// GetCreateOptions returns the regions, sizes, features, and account limits -// available to the authenticated team when creating a MicroDroplet. -func (s *MicroDropletsServiceOp) GetCreateOptions(ctx context.Context) (*MicroDropletCreateOptions, *Response, error) { - req, err := s.client.NewRequest(ctx, http.MethodGet, microDropletOptionsPath, nil) - if err != nil { - return nil, nil, err - } - - opts := new(MicroDropletCreateOptions) - resp, err := s.client.Do(ctx, req, opts) - if err != nil { - return nil, resp, err - } - - return opts, resp, nil -} diff --git a/vendor/github.com/digitalocean/godo/microvms.go b/vendor/github.com/digitalocean/godo/microvms.go new file mode 100644 index 000000000..e440720e9 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/microvms.go @@ -0,0 +1,744 @@ +package godo + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" +) + +const ( + microVMBasePath = "v2/microvms" + microVMCheckpointsBasePath = "v2/microvms/checkpoints" + microVMOptionsPath = "v2/microvms/options" +) + +// MicroVMState represents the lifecycle state of a MicroVM. +type MicroVMState string + +// Possible lifecycle states for a MicroVM. +const ( + MicroVMStateUnknown = MicroVMState("unknown") + MicroVMStateCreating = MicroVMState("creating") + MicroVMStateRunning = MicroVMState("running") + MicroVMStatePausing = MicroVMState("pausing") + MicroVMStatePaused = MicroVMState("paused") + MicroVMStateResuming = MicroVMState("resuming") + MicroVMStateTerminating = MicroVMState("terminating") + MicroVMStateTerminated = MicroVMState("terminated") + MicroVMStateFailed = MicroVMState("failed") +) + +// MicroVMNetworking represents the networking mode of a MicroVM. +type MicroVMNetworking string + +// Possible networking modes for a MicroVM. +const ( + MicroVMNetworkingUnknown = MicroVMNetworking("unknown") + MicroVMNetworkingPublic = MicroVMNetworking("public") + MicroVMNetworkingVPC = MicroVMNetworking("vpc") +) + +// MicroVMHTTPProtocol represents the HTTP protocol option for a MicroVM. +type MicroVMHTTPProtocol string + +// Possible HTTP protocol values for a MicroVM. +const ( + MicroVMHTTPProtocolHTTP = MicroVMHTTPProtocol("http") + MicroVMHTTPProtocolHTTP2 = MicroVMHTTPProtocol("http2") +) + +// MicroVMURLStatus represents the lifecycle of a MicroVM URL. +type MicroVMURLStatus string + +// Possible statuses for a MicroVM URL. +const ( + MicroVMURLStatusPending = MicroVMURLStatus("PENDING") + MicroVMURLStatusActive = MicroVMURLStatus("ACTIVE") +) + +// MicroVMCheckpointStatus represents the status of a MicroVM checkpoint. +type MicroVMCheckpointStatus string + +// Possible states for a MicroVM checkpoint. +const ( + MicroVMCheckpointStatusUnknown = MicroVMCheckpointStatus("CHECKPOINT_UNKNOWN") + MicroVMCheckpointStatusCreating = MicroVMCheckpointStatus("CHECKPOINT_CREATING") + MicroVMCheckpointStatusAvailable = MicroVMCheckpointStatus("CHECKPOINT_AVAILABLE") + MicroVMCheckpointStatusFailed = MicroVMCheckpointStatus("CHECKPOINT_FAILED") + MicroVMCheckpointStatusDeleted = MicroVMCheckpointStatus("CHECKPOINT_DELETED") + MicroVMCheckpointStatusDeleting = MicroVMCheckpointStatus("CHECKPOINT_DELETING") +) + +// MicroVMsService is an interface for interfacing with the MicroVM +// endpoints of the DigitalOcean API. +// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/MicroVMs +type MicroVMsService interface { + List(ctx context.Context, opt *ListOptions) ([]MicroVM, *Response, error) + ListByRegion(ctx context.Context, region string, opt *ListOptions) ([]MicroVM, *Response, error) + ListByName(ctx context.Context, name string, opt *ListOptions) ([]MicroVM, *Response, error) + ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]MicroVM, *Response, error) + ListFiltered(ctx context.Context, opt *ListMicroVMsOptions) ([]MicroVM, *Response, error) + Get(ctx context.Context, id string) (*MicroVM, *Response, error) + Create(ctx context.Context, createRequest *MicroVMCreateRequest) (*MicroVM, *Response, error) + Pause(ctx context.Context, id string) (*MicroVM, *Response, error) + Resume(ctx context.Context, id string) (*MicroVM, *Response, error) + Delete(ctx context.Context, id string) (*Response, error) + + ListCheckpoints(ctx context.Context, opt *ListMicroVMCheckpointsOptions) ([]MicroVMCheckpoint, *Response, error) + CreateCheckpoint(ctx context.Context, microVMID string, createRequest *MicroVMCheckpointCreateRequest) (*MicroVMCheckpoint, *Response, error) + GetCheckpoint(ctx context.Context, id string) (*MicroVMCheckpoint, *Response, error) + DeleteCheckpoint(ctx context.Context, id string) (*Response, error) + + GetCreateOptions(ctx context.Context) (*MicroVMCreateOptions, *Response, error) + + Exec(ctx context.Context, id string, execRequest *MicroVMExecRequest) (*MicroVMExecResult, *Response, error) + ConsoleURL(id string, opt *MicroVMConsoleOptions) (string, error) +} + +// MicroVMsServiceOp handles communication with the MicroVM related +// methods of the DigitalOcean API. +type MicroVMsServiceOp struct { + client *Client +} + +var _ MicroVMsService = &MicroVMsServiceOp{} + +// MicroVMSize is the compute capacity reported on a MicroVM. +type MicroVMSize struct { + CPU uint32 `json:"cpu,omitempty"` + Memory uint32 `json:"memory,omitempty"` + Disk uint64 `json:"disk,omitempty"` +} + +// MicroVMSizeRequest is the compute capacity requested at create time. +// Disk is provisioned with the size and is not requested. +type MicroVMSizeRequest struct { + CPU uint32 `json:"cpu"` + Memory uint32 `json:"memory"` +} + +// MicroVMSource names what a MicroVM runs. Exactly one of OCIRef or +// CheckpointID must be set on create; the API rejects both or neither. +type MicroVMSource struct { + OCIRef string `json:"oci_ref,omitempty"` + CheckpointID string `json:"checkpoint_id,omitempty"` +} + +// MicroVMURL is one ingress URL attached to a MicroVM. +type MicroVMURL struct { + Hostname string `json:"hostname,omitempty"` + Port int `json:"port,omitempty"` + Default bool `json:"default,omitempty"` + Status MicroVMURLStatus `json:"status,omitempty"` +} + +// MicroVM represents a DigitalOcean MicroVM. +type MicroVM struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + State MicroVMState `json:"state,omitempty"` + Size *MicroVMSize `json:"size,omitempty"` + URLs []MicroVMURL `json:"urls,omitempty"` + Ports []uint32 `json:"ports,omitempty"` + FailureReason string `json:"failure_reason,omitempty"` + Networking MicroVMNetworking `json:"networking,omitempty"` + Source *MicroVMSource `json:"source,omitempty"` + AutoPause *AutoPauseConfig `json:"auto_pause,omitempty"` + AutoResume *bool `json:"auto_resume,omitempty"` + HTTPProtocol MicroVMHTTPProtocol `json:"http_protocol,omitempty"` + Created string `json:"created_at,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// AutoPauseConfig configures MicroVM auto-pause behavior. IdleTimeout is +// a Go duration string (e.g. "5m", "30s") describing how long the MicroVM +// must be idle before it is paused. +type AutoPauseConfig struct { + Enabled *bool `json:"enabled,omitempty"` + IdleTimeout string `json:"idle_timeout,omitempty"` +} + +// MicroVMCheckpoint represents a checkpoint of a MicroVM +// (persisted memory + disk state). +type MicroVMCheckpoint struct { + ID string `json:"id,omitempty"` + MicroVMID string `json:"microvm_id,omitempty"` + MicroVMName string `json:"microvm_name,omitempty"` + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + // Size is the guest shape this checkpoint restores at. Omitted when the + // checkpoint does not record one; a create from it must then name a size. + Size *MicroVMSize `json:"size,omitempty"` + Status MicroVMCheckpointStatus `json:"status,omitempty"` + MemoryBytes uint64 `json:"memory_bytes,omitempty"` + DiskBytes uint64 `json:"disk_bytes,omitempty"` + Created string `json:"created_at,omitempty"` +} + +// MicroVMCreateRequest represents a request to create a MicroVM. +// Size and Region may be omitted when restoring from a checkpoint so the API +// can inherit them from the checkpoint (api-v2 C6). +type MicroVMCreateRequest struct { + Name string `json:"name"` + Region string `json:"region,omitempty"` + Size *MicroVMSizeRequest `json:"size,omitempty"` + Source *MicroVMSource `json:"source"` + Networking MicroVMNetworking `json:"networking,omitempty"` + VPCUUID string `json:"vpc_uuid,omitempty"` + AutoPause *AutoPauseConfig `json:"auto_pause,omitempty"` + AutoResume *bool `json:"auto_resume,omitempty"` + HTTPPort uint32 `json:"http_port,omitempty"` + HTTPProtocol MicroVMHTTPProtocol `json:"http_protocol,omitempty"` + Ports []uint32 `json:"ports,omitempty"` + Environment map[string]string `json:"environment,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// MicroVMCheckpointCreateRequest represents a request to create a checkpoint. +type MicroVMCheckpointCreateRequest struct { + Name string `json:"name,omitempty"` +} + +// ListMicroVMCheckpointsOptions are the optional parameters for listing +// checkpoints. MicroVMID filters to checkpoints captured from that +// MicroVM; omit it to list all checkpoints for the team. +type ListMicroVMCheckpointsOptions struct { + ListOptions + MicroVMID string `url:"microvm_id,omitempty"` +} + +// MicroVMCreateOptions is the response from GET /v2/microvms/options. +type MicroVMCreateOptions struct { + DefaultRegion string `json:"default_region,omitempty"` + Sizes []MicroVMSizeOption `json:"sizes,omitempty"` + Features []MicroVMFeatureOption `json:"features,omitempty"` + AccountLimits *MicroVMAccountLimits `json:"account_limits,omitempty"` +} + +// MicroVMSizeOption is one supported size evaluated for the team. Regions +// holds the slugs where the team can create this size; a region it cannot use +// is absent rather than listed as unavailable. +type MicroVMSizeOption struct { + CPU uint32 `json:"cpu"` + Memory uint32 `json:"memory"` + Disk uint64 `json:"disk"` + Available bool `json:"available"` + Regions []string `json:"regions"` + Pricing *MicroVMSizePricing `json:"pricing,omitempty"` +} + +// MicroVMSizePricing is the USD price of one size. +type MicroVMSizePricing struct { + PricePerHour float64 `json:"price_per_hour"` + PricePerMonth float64 `json:"price_per_month"` +} + +// MicroVMFeatureOption is one product feature gate for the team. +type MicroVMFeatureOption struct { + Name string `json:"name,omitempty"` + Enabled bool `json:"enabled"` +} + +// MicroVMAccountLimits are the team's effective MicroVM limits. +type MicroVMAccountLimits struct { + MaxConcurrentRunning uint64 `json:"max_concurrent_running,omitempty"` + MaxTotalCount uint64 `json:"max_total_count,omitempty"` + MaxMemoryBytes uint64 `json:"max_memory_bytes,omitempty"` + MaxDiskBytes uint64 `json:"max_disk_bytes,omitempty"` + MaxIdleTimeoutSeconds uint64 `json:"max_idle_timeout_seconds,omitempty"` +} + +// MicroVMExecRequest is a one-shot, non-PTY command run in a MicroVM's +// workload container. Argv must be non-empty (e.g. ["echo", "hi"]). +type MicroVMExecRequest struct { + Argv []string `json:"argv"` + Cwd string `json:"cwd,omitempty"` +} + +// MicroVMExecResult is the outcome of a one-shot exec. A non-zero ExitCode +// is not a client error — the API returns HTTP 200 with the exit code set. +// Truncated is true when the server clipped stdout/stderr to its output bound. +type MicroVMExecResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + ExitCode int32 `json:"exit_code"` + Truncated bool `json:"truncated,omitempty"` +} + +// MicroVMConsoleOptions sets the initial PTY size for ConsoleURL. +// The server defaults to 24 rows by 80 columns when either is zero / unset. +type MicroVMConsoleOptions struct { + Rows uint32 + Cols uint32 +} + +// Stable console WebSocket error.code values (server → client text frames). +const ( + MicroVMConsoleErrDialFailed = "dial_failed" + MicroVMConsoleErrStartFailed = "start_failed" + MicroVMConsoleErrAgentError = "agent_error" +) + +// MicroVMConsoleControl is a decoded server → client text control frame. +// Exactly one of Error, Exit, or Status is set for a well-formed frame. +type MicroVMConsoleControl struct { + Error *MicroVMConsoleError `json:"error,omitempty"` + Exit *MicroVMConsoleExit `json:"exit,omitempty"` + Status *MicroVMConsoleStatus `json:"status,omitempty"` +} + +// MicroVMConsoleError is the payload of an {"error":...} control frame. +type MicroVMConsoleError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// MicroVMConsoleExit is the payload of an {"exit":...} control frame. +type MicroVMConsoleExit struct { + Code int32 `json:"code"` +} + +// MicroVMConsoleStatus is the payload of a {"status":...} control frame +// (e.g. state "resuming" while a paused MicroVM is auto-resumed). +type MicroVMConsoleStatus struct { + State string `json:"state"` +} + +// String returns a human-readable description of a MicroVM. +func (m MicroVM) String() string { + return Stringify(m) +} + +// URN returns the MicroVM ID in a valid DO API URN form. +func (m MicroVM) URN() string { + return ToURN("MicroVM", m.ID) +} + +// String returns a human-readable description of a MicroVMCheckpoint. +func (c MicroVMCheckpoint) String() string { + return Stringify(c) +} + +// String returns a human-readable description of a MicroVMCreateRequest. +func (r MicroVMCreateRequest) String() string { + return Stringify(r) +} + +type microVMRoot struct { + MicroVM *MicroVM `json:"microvm"` +} + +type microVMsRoot struct { + MicroVMs []MicroVM `json:"microvms"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +type microVMCheckpointRoot struct { + Checkpoint *MicroVMCheckpoint `json:"checkpoint"` +} + +type microVMCheckpointsRoot struct { + Checkpoints []MicroVMCheckpoint `json:"checkpoints"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +// ListMicroVMsOptions filters GET /v2/microvms. Pagination lives on the +// embedded ListOptions. Region, Name, and TagName are omitted when empty +// and combine: the API ANDs whichever are set. +type ListMicroVMsOptions struct { + ListOptions + Region string `url:"region,omitempty"` + Name string `url:"name,omitempty"` + TagName string `url:"tag_name,omitempty"` +} + +// List lists all MicroVMs, with optional pagination. +func (s *MicroVMsServiceOp) List(ctx context.Context, opt *ListOptions) ([]MicroVM, *Response, error) { + var filtered *ListMicroVMsOptions + if opt != nil { + filtered = &ListMicroVMsOptions{ListOptions: *opt} + } + return s.ListFiltered(ctx, filtered) +} + +// ListByRegion lists MicroVMs filtered by region slug, with optional pagination. +func (s *MicroVMsServiceOp) ListByRegion(ctx context.Context, region string, opt *ListOptions) ([]MicroVM, *Response, error) { + if region == "" { + return nil, nil, NewArgError("region", "cannot be empty") + } + filtered := &ListMicroVMsOptions{Region: region} + if opt != nil { + filtered.ListOptions = *opt + } + return s.ListFiltered(ctx, filtered) +} + +// ListByName lists MicroVMs filtered by exact name match, with optional pagination. +func (s *MicroVMsServiceOp) ListByName(ctx context.Context, name string, opt *ListOptions) ([]MicroVM, *Response, error) { + if name == "" { + return nil, nil, NewArgError("name", "cannot be empty") + } + filtered := &ListMicroVMsOptions{Name: name} + if opt != nil { + filtered.ListOptions = *opt + } + return s.ListFiltered(ctx, filtered) +} + +// ListByTag lists MicroVMs that carry the given resource tag. A tag that +// matches nothing returns an empty list rather than an error. +func (s *MicroVMsServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]MicroVM, *Response, error) { + if tag == "" { + return nil, nil, NewArgError("tag", "cannot be empty") + } + filtered := &ListMicroVMsOptions{TagName: tag} + if opt != nil { + filtered.ListOptions = *opt + } + return s.ListFiltered(ctx, filtered) +} + +// ListFiltered lists MicroVMs using any combination of region, name, and tag. +func (s *MicroVMsServiceOp) ListFiltered(ctx context.Context, opt *ListMicroVMsOptions) ([]MicroVM, *Response, error) { + path := microVMBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microVMsRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + if m := root.Meta; m != nil { + resp.Meta = m + } + + return root.MicroVMs, resp, nil +} + +// Get retrieves a MicroVM by its ID. +func (s *MicroVMsServiceOp) Get(ctx context.Context, id string) (*MicroVM, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microVMBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microVMRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.MicroVM, resp, nil +} + +// Create provisions a new MicroVM with the provided configuration. +func (s *MicroVMsServiceOp) Create(ctx context.Context, createRequest *MicroVMCreateRequest) (*MicroVM, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, microVMBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(microVMRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.MicroVM, resp, nil +} + +// Pause synchronously transitions a RUNNING MicroVM to PAUSED. It blocks +// until the platform has durably paused the MicroVM and returns the +// updated resource. The call is idempotent: pausing a MicroVM that is +// already PAUSED returns the current MicroVM with no side effects. +func (s *MicroVMsServiceOp) Pause(ctx context.Context, id string) (*MicroVM, *Response, error) { + return s.doTransition(ctx, id, "pause") +} + +// Resume synchronously transitions a PAUSED MicroVM to RUNNING. It blocks +// until the platform has durably resumed the MicroVM and returns the +// updated resource. The call is idempotent: resuming a MicroVM that is +// already RUNNING returns the current MicroVM with no side effects. +func (s *MicroVMsServiceOp) Resume(ctx context.Context, id string) (*MicroVM, *Response, error) { + return s.doTransition(ctx, id, "resume") +} + +// doTransition posts an empty body to a MicroVM transition sub-resource +// (e.g. /pause, /resume) and decodes the returned MicroVM. +func (s *MicroVMsServiceOp) doTransition(ctx context.Context, id, action string) (*MicroVM, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s/%s", microVMBasePath, id, action) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microVMRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.MicroVM, resp, nil +} + +// Delete removes a MicroVM by its ID. The DigitalOcean API returns a 204 +// on success and does not include a response body. +func (s *MicroVMsServiceOp) Delete(ctx context.Context, id string) (*Response, error) { + if id == "" { + return nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microVMBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// ListCheckpoints lists checkpoints for the authenticated team. Pass a +// MicroVMID on opt to filter to checkpoints captured from that MicroVM. +func (s *MicroVMsServiceOp) ListCheckpoints(ctx context.Context, opt *ListMicroVMCheckpointsOptions) ([]MicroVMCheckpoint, *Response, error) { + path := microVMCheckpointsBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microVMCheckpointsRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + if m := root.Meta; m != nil { + resp.Meta = m + } + + return root.Checkpoints, resp, nil +} + +// CreateCheckpoint starts an asynchronous checkpoint of a running MicroVM. +func (s *MicroVMsServiceOp) CreateCheckpoint(ctx context.Context, microVMID string, createRequest *MicroVMCheckpointCreateRequest) (*MicroVMCheckpoint, *Response, error) { + if microVMID == "" { + return nil, nil, NewArgError("microVMID", "cannot be empty") + } + if createRequest == nil { + createRequest = &MicroVMCheckpointCreateRequest{} + } + + path := fmt.Sprintf("%s/%s/checkpoints", microVMBasePath, microVMID) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(microVMCheckpointRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Checkpoint, resp, nil +} + +// GetCheckpoint retrieves a checkpoint by its ID. +func (s *MicroVMsServiceOp) GetCheckpoint(ctx context.Context, id string) (*MicroVMCheckpoint, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microVMCheckpointsBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microVMCheckpointRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Checkpoint, resp, nil +} + +// DeleteCheckpoint releases the state stored by a checkpoint. Returns a 204 +// on success with no response body. +func (s *MicroVMsServiceOp) DeleteCheckpoint(ctx context.Context, id string) (*Response, error) { + if id == "" { + return nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microVMCheckpointsBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// GetCreateOptions returns the sizes, the regions each one can be created in, +// the feature gates, and the account limits available to the authenticated +// team when creating a MicroVM. +func (s *MicroVMsServiceOp) GetCreateOptions(ctx context.Context) (*MicroVMCreateOptions, *Response, error) { + req, err := s.client.NewRequest(ctx, http.MethodGet, microVMOptionsPath, nil) + if err != nil { + return nil, nil, err + } + + opts := new(MicroVMCreateOptions) + resp, err := s.client.Do(ctx, req, opts) + if err != nil { + return nil, resp, err + } + + return opts, resp, nil +} + +// Exec runs a one-shot, non-PTY command in the MicroVM's workload container +// and returns its stdout, stderr, and exit code. Requires the exec_pty feature +// gate (see GetCreateOptions Features). A paused MicroVM is auto-resumed. +// A non-zero ExitCode is not a Go error — the API returns HTTP 200. +func (s *MicroVMsServiceOp) Exec(ctx context.Context, id string, execRequest *MicroVMExecRequest) (*MicroVMExecResult, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + if execRequest == nil { + return nil, nil, NewArgError("execRequest", "cannot be nil") + } + if len(execRequest.Argv) == 0 { + return nil, nil, NewArgError("execRequest.Argv", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s/exec", microVMBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, execRequest) + if err != nil { + return nil, nil, err + } + + result := new(MicroVMExecResult) + resp, err := s.client.Do(ctx, req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// ConsoleURL builds the WebSocket URL for an interactive PTY console on the +// MicroVM (GET /v2/microvms/{id}/console). godo does not dial WebSockets; the +// caller supplies its own client. The bearer token lives on godo's HTTP +// transport and is not attached to requests built here, so the caller must set +// Authorization: Bearer on the WebSocket handshake. Binary frames are +// raw terminal I/O; text frames carry resize (client→server) and error/exit/ +// status control (server→client) — see MarshalMicroVMConsoleResize and +// ParseMicroVMConsoleControl. +func (s *MicroVMsServiceOp) ConsoleURL(id string, opt *MicroVMConsoleOptions) (string, error) { + if id == "" { + return "", NewArgError("id", "cannot be empty") + } + if s.client == nil || s.client.BaseURL == nil { + return "", NewArgError("client.BaseURL", "cannot be nil") + } + + u, err := s.client.BaseURL.Parse(fmt.Sprintf("%s/%s/console", microVMBasePath, id)) + if err != nil { + return "", err + } + + switch u.Scheme { + case "https": + u.Scheme = "wss" + case "http": + u.Scheme = "ws" + } + + if opt != nil { + q := url.Values{} + if opt.Rows > 0 { + q.Set("rows", strconv.FormatUint(uint64(opt.Rows), 10)) + } + if opt.Cols > 0 { + q.Set("cols", strconv.FormatUint(uint64(opt.Cols), 10)) + } + u.RawQuery = q.Encode() + } + + return u.String(), nil +} + +// MarshalMicroVMConsoleResize encodes a client → server text frame that +// resizes the console PTY: {"resize":{"rows":N,"cols":M}}. +func MarshalMicroVMConsoleResize(rows, cols uint32) ([]byte, error) { + return json.Marshal(struct { + Resize struct { + Rows uint32 `json:"rows"` + Cols uint32 `json:"cols"` + } `json:"resize"` + }{Resize: struct { + Rows uint32 `json:"rows"` + Cols uint32 `json:"cols"` + }{Rows: rows, Cols: cols}}) +} + +// ParseMicroVMConsoleControl decodes a server → client text control frame +// (error, exit, or status). Binary frames are raw PTY stdout and should not +// be passed here. Returns an error if the payload is not valid JSON or does +// not contain any recognized control field. +func ParseMicroVMConsoleControl(data []byte) (*MicroVMConsoleControl, error) { + ctrl := new(MicroVMConsoleControl) + if err := json.Unmarshal(data, ctrl); err != nil { + return nil, err + } + if ctrl.Error == nil && ctrl.Exit == nil && ctrl.Status == nil { + return nil, fmt.Errorf("microvm console: unrecognized control frame") + } + return ctrl, nil +} diff --git a/vendor/github.com/digitalocean/godo/nfs.go b/vendor/github.com/digitalocean/godo/nfs.go index c47b8a8a5..a357b8408 100644 --- a/vendor/github.com/digitalocean/godo/nfs.go +++ b/vendor/github.com/digitalocean/godo/nfs.go @@ -176,6 +176,7 @@ type NfsAccessPoint struct { UpdatedAt string `json:"updated_at"` IsDefault bool `json:"is_default"` VpcID *string `json:"vpc_id,omitempty"` + VpcIDs []string `json:"vpc_ids,omitempty"` } // NfsCreateRequest represents a request to create an NFS share. diff --git a/vendor/github.com/digitalocean/godo/prepayment.go b/vendor/github.com/digitalocean/godo/prepayment.go new file mode 100644 index 000000000..2b9cf4d18 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/prepayment.go @@ -0,0 +1,96 @@ +package godo + +import ( + "context" + "net/http" + "time" +) + +// PrepaymentService is an interface for interfacing with the Prepayment +// endpoints of the DigitalOcean API. +type PrepaymentService interface { + GetConfig(context.Context) (*PrepaymentConfigResponse, *Response, error) + GetStatus(context.Context) (*PrepaymentStatusResponse, *Response, error) +} + +// PrepaymentServiceOp handles communication with the Prepayment related methods of +// the DigitalOcean API. +type PrepaymentServiceOp struct { + client *Client +} + +var _ PrepaymentService = &PrepaymentServiceOp{} + +// PrepaymentConfig represents prepayment gate configuration for an account. +type PrepaymentConfig struct { + SpendLimit string `json:"spend_limit"` + IsAutoPrepayEnabled bool `json:"is_auto_prepay_enabled"` + PrepayAmount string `json:"prepay_amount"` + PrepayThreshold string `json:"prepay_threshold"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// PrepaymentStatus represents prepayment gate status for an account. +type PrepaymentStatus struct { + Balance string `json:"balance"` + IsAutoPrepayEnabled bool `json:"is_auto_prepay_enabled"` + Blocked bool `json:"blocked"` + Eligible bool `json:"eligible"` + MonthToDateBalance string `json:"month_to_date_balance"` +} + +// PrepaymentConfigResponse is the response from the public prepayment config endpoint. +type PrepaymentConfigResponse struct { + Config *PrepaymentConfig `json:"config"` + Status *PrepaymentStatus `json:"status"` +} + +func (r PrepaymentConfigResponse) String() string { + return Stringify(r) +} + +// PrepaymentStatusResponse is the response from the public prepayment status endpoint. +type PrepaymentStatusResponse struct { + Status *PrepaymentStatus `json:"status"` +} + +func (r PrepaymentStatusResponse) String() string { + return Stringify(r) +} + +// GetConfig retrieves the authenticated customer's prepayment configuration and status. +func (s *PrepaymentServiceOp) GetConfig(ctx context.Context) (*PrepaymentConfigResponse, *Response, error) { + path := "v2/customers/my/prepayment_config" + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(PrepaymentConfigResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} + +// GetStatus retrieves the authenticated customer's prepayment status. +func (s *PrepaymentServiceOp) GetStatus(ctx context.Context) (*PrepaymentStatusResponse, *Response, error) { + path := "v2/customers/my/prepayment_status" + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(PrepaymentStatusResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} diff --git a/vendor/github.com/digitalocean/godo/streaming.go b/vendor/github.com/digitalocean/godo/streaming.go index d35c66e2f..c13070a4e 100644 --- a/vendor/github.com/digitalocean/godo/streaming.go +++ b/vendor/github.com/digitalocean/godo/streaming.go @@ -20,6 +20,13 @@ type SSEEvent struct { // SSEReader parses a "text/event-stream" byte stream into SSEEvents. // Bare-CR line terminators are not supported. Not safe for concurrent use. type SSEReader struct { + // OnComment, when set, is called with the text of each comment line + // (after the leading ':' and one optional space). Comments are still + // never dispatched as events; this only makes them observable, for + // servers that carry out-of-band metadata on comment lines. The slice + // is only valid for the duration of the call. + OnComment func(comment []byte) + r *bufio.Reader lastID string scratch []byte @@ -75,6 +82,13 @@ func (s *SSEReader) Next() (*SSEEvent, error) { } if line[0] == ':' { + if s.OnComment != nil { + comment := line[1:] + if len(comment) > 0 && comment[0] == ' ' { + comment = comment[1:] + } + s.OnComment(comment) + } if eof { s.err = io.EOF return nil, io.EOF diff --git a/vendor/modules.txt b/vendor/modules.txt index 5ead63ac8..0ccbe16d0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -33,7 +33,7 @@ github.com/coreos/go-systemd/v22/journal # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew -# github.com/digitalocean/godo v1.208.0 +# github.com/digitalocean/godo v1.215.0 ## explicit; go 1.23.0 github.com/digitalocean/godo github.com/digitalocean/godo/metrics