Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
409d999
Add OHS endpoints
SSharma-10 May 27, 2026
8880800
gofmt
SSharma-10 May 27, 2026
6091b1d
Merge branch 'main' into OHS_endpoints
SSharma-10 May 27, 2026
ebadc3c
Merge branch 'main' into OHS_endpoints
SSharma-10 Jun 3, 2026
7c71b3c
Remove StartOAuthFlow from Hosted Agents SDK for v0.
SSharma-10 Jun 3, 2026
f05e561
Merge branch 'main' into OHS_endpoints
SSharma-10 Jun 3, 2026
99a6b7d
add support to accept manifest as raw yaml
SSharma-10 Jun 4, 2026
60bd8b1
Merge branch 'main' into OHS_endpoints
SSharma-10 Jun 4, 2026
1d3bb13
hosted agents: decode the SPI canonical event stream
logwolvy Jun 8, 2026
1c680f8
errors: surface nested {"error":{...}} response envelope
logwolvy Jun 8, 2026
be2fa50
Merge branch 'main' into pr-1021
SSharma-10 Jun 9, 2026
53251e9
Add upload and download workspace endpts
SSharma-10 Jun 25, 2026
228ad8e
Merge remote-tracking branch 'origin/main' into OHS_endpoints
SSharma-10 Jun 25, 2026
6f27722
hosted_agents: remove SandboxID from HostedAgentSession (#1042)
vrindavinod-do Jun 30, 2026
187c438
add support for session name
SSharma-10 Jul 2, 2026
1aca8f5
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 2, 2026
ea87d8e
add support for resume/pause
SSharma-10 Jul 3, 2026
dca8a77
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 3, 2026
205362b
fix hosted agents workspace download when X-Content-Sha256 trailer is…
SSharma-10 Jul 7, 2026
9699d3a
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 7, 2026
58e2c9c
hosted agents: verify workspace download SHA via body footer
SSharma-10 Jul 21, 2026
0b3393d
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 21, 2026
44b44ef
add support to download/upload large files
SSharma-10 Jul 23, 2026
368f121
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 23, 2026
9f902a0
hosted agents: batch part-upload URLs for workspace transfers
SSharma-10 Jul 28, 2026
49f4430
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 28, 2026
7738f6f
MARSOHS-551: Session.origin on HostedAgents (stack on OHS_endpoints) …
nveerdixit Jul 28, 2026
4a6471b
hosted agents: stream live events from the data-plane /events endpoint
logwolvy Jul 29, 2026
88bd625
hosted agents: serve replay-only reads from the data-plane /events en…
logwolvy Jul 30, 2026
c55e84c
add support for webhooks and triggers
SSharma-10 Jul 30, 2026
3dc1320
Merge branch 'main' into OHS_endpoints
SSharma-10 Jul 30, 2026
eae8993
Revert "hosted agents: serve replay-only reads from the data-plane /e…
SSharma-10 Jul 31, 2026
ab376b8
Revert "hosted agents: stream live events from the data-plane /events…
SSharma-10 Jul 31, 2026
c080631
add support for OpenAI Agent provider
SSharma-10 Aug 7, 2026
3a9c42b
Merge branch 'main' into OHS_endpoints
SSharma-10 Aug 7, 2026
7e1bea9
Ohs endpoints session history paging (#1078)
mishraishika24-pixel Aug 7, 2026
4f3cfc0
Merge branch 'main' into OHS_endpoints
SSharma-10 Aug 11, 2026
b05d159
Add hosted-agent session checkpoint, fork, and rollback client APIs
SSharma-10 Aug 14, 2026
78e3bb6
hosted agents: add provider OAuth connect endpoints (#1084)
jkosanam Aug 14, 2026
07c004b
hosted agents: carry native protocol frames alongside the canonical s…
julia-ye Aug 17, 2026
1d4643b
hosted agents: read session events from the data-plane /events endpoi…
logwolvy Aug 18, 2026
baf30c2
Add Hosted Agents Agent Configs client + config-backed session create…
gane5hvarma Aug 18, 2026
5f73335
changes (#1087)
mishraishika24-pixel Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions examples/hosted-agents/auth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"context"
"fmt"
"net/url"
"os"
"time"

"github.com/digitalocean/godo"
)

func main() {
provider := os.Getenv("HOSTED_AGENT_PROVIDER")
if provider == "" {
provider = "github"
}

client := mustClient()
ctx := context.Background()

start, _, err := client.HostedAgents.StartProviderAuth(ctx, provider)
if err != nil {
die(err)
}

if start.Status == "success" {
fmt.Printf("%s is already connected for your team\n", provider)
return
}

fmt.Printf("To connect %s, open this URL and authorize access:\n\n %s\n\n", provider, start.ConnectURL)
if start.VerificationCode != "" {
fmt.Printf("Verify the page shows code: %s\n\n", start.VerificationCode)
}

fmt.Println("Waiting for authorization to complete...")
for {
poll, _, err := client.HostedAgents.PollProviderAuth(ctx, provider, start.PollURL)
if err != nil {
die(err)
}
if poll.Status == "success" {
fmt.Printf("%s connected successfully\n", provider)
return
}
time.Sleep(2 * time.Second)
}
}

func mustClient() *godo.Client {
token := os.Getenv("DIGITALOCEAN_TOKEN")
if token == "" {
fmt.Fprintln(os.Stderr, "DIGITALOCEAN_TOKEN is required")
os.Exit(2)
}
client := godo.NewFromToken(token)
if baseURL := os.Getenv("DIGITALOCEAN_API_URL"); baseURL != "" {
u, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
client.BaseURL = u
}
return client
}

func die(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
66 changes: 66 additions & 0 deletions examples/hosted-agents/create-session/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"context"
"errors"
"fmt"
"net/url"
"os"

"github.com/digitalocean/godo"
)

func main() {
token := os.Getenv("DIGITALOCEAN_TOKEN")
if token == "" {
fmt.Fprintln(os.Stderr, "DIGITALOCEAN_TOKEN is required")
os.Exit(2)
}

client := godo.NewFromToken(token)
if baseURL := os.Getenv("DIGITALOCEAN_API_URL"); baseURL != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this will get removed from examples once api.digitalocean.com/v2/... endpoints are available, because default URL domain is api.digitalocean.com

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

u, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
client.BaseURL = u
}

agentKind := godo.HostedAgentKindClaudeCode
if v := os.Getenv("HOSTED_AGENT_KIND"); v != "" {
agentKind = godo.HostedAgentKind(v)
}

ctx := context.Background()

if client.HostedAgents == nil {
fmt.Fprintln(os.Stderr, "HostedAgents service is not initialized on this godo client")
os.Exit(1)
}

session, resp, err := client.HostedAgents.CreateSession(ctx, &godo.HostedAgentSessionCreateRequest{
AgentKind: agentKind,
RepoHint: os.Getenv("REPO_HINT"),
})
if err != nil {
var apiErr *godo.ErrorResponse
if errors.As(err, &apiErr) {
fmt.Fprintf(os.Stderr, "API error (HTTP %d): %s\n", apiErr.Response.StatusCode, apiErr.Message)
os.Exit(1)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

fmt.Printf("HTTP %d\n", resp.StatusCode)
if session == nil {
fmt.Fprintln(os.Stderr, "API returned no session")
os.Exit(1)
}
fmt.Printf("session_id: %s\n", session.SessionID)
fmt.Printf("status: %s\n", session.Status)
fmt.Printf("agent_kind: %s\n", session.AgentKind)
if session.RepoHint != "" {
fmt.Printf("repo_hint: %s\n", session.RepoHint)
}
}
56 changes: 56 additions & 0 deletions examples/hosted-agents/destroy-session/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"context"
"errors"
"fmt"
"net/url"
"os"

"github.com/digitalocean/godo"
)

func main() {
sessionID := os.Getenv("HOSTED_AGENT_SESSION_ID")
if sessionID == "" {
fmt.Fprintln(os.Stderr, "HOSTED_AGENT_SESSION_ID is required")
os.Exit(2)
}

client := mustClient()
ctx := context.Background()

resp, err := client.HostedAgents.DestroySession(ctx, sessionID)
if err != nil {
die(err)
}

fmt.Printf("HTTP %d — session %s destroyed\n", resp.StatusCode, sessionID)
}

func mustClient() *godo.Client {
token := os.Getenv("DIGITALOCEAN_TOKEN")
if token == "" {
fmt.Fprintln(os.Stderr, "DIGITALOCEAN_TOKEN is required")
os.Exit(2)
}
client := godo.NewFromToken(token)
if baseURL := os.Getenv("DIGITALOCEAN_API_URL"); baseURL != "" {
u, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
client.BaseURL = u
}
return client
}

func die(err error) {
var apiErr *godo.ErrorResponse
if errors.As(err, &apiErr) {
fmt.Fprintf(os.Stderr, "API error (HTTP %d): %s\n", apiErr.Response.StatusCode, apiErr.Message)
} else {
fmt.Fprintln(os.Stderr, err)
}
os.Exit(1)
}
106 changes: 106 additions & 0 deletions examples/hosted-agents/download-workspace/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package main

import (
"context"
"errors"
"fmt"
"io"
"net/url"
"os"

"github.com/digitalocean/godo"
)

// download-workspace streams a file (or tar archive) out of a session's sandbox
// workspace and writes it to disk.
//
// The download is chunked and ends with a fixed 73-byte integrity footer
// (DOWSSHA1 + SHA-256 hex + newline). The godo client strips that footer and
// verifies the payload checksum for you: you MUST read the body to EOF
// (io.Copy below) and then Close it. A missing, invalid, or mismatched footer
// surfaces as an error — when that happens the partial output must be discarded.
//
// Required env:
// - DIGITALOCEAN_TOKEN
// - HOSTED_AGENT_SESSION_ID
// - WORKSPACE_PATH source path inside /workspace
// - LOCAL_FILE local destination path
//
// Optional env:
// - AS_ARCHIVE=true tar-stream the directory at WORKSPACE_PATH
func main() {
sessionID := mustEnv("HOSTED_AGENT_SESSION_ID")
workspacePath := mustEnv("WORKSPACE_PATH")
localFile := mustEnv("LOCAL_FILE")

client := mustClient()
ctx := context.Background()

dl, resp, err := client.HostedAgents.DownloadWorkspace(ctx, sessionID, &godo.HostedAgentWorkspaceDownloadRequest{
Path: workspacePath,
AsArchive: os.Getenv("AS_ARCHIVE") == "true",
})
if err != nil {
die(err)
}
defer dl.Body.Close()

fmt.Printf("HTTP %d\n", resp.StatusCode)
fmt.Printf("is_archive: %t\n", dl.IsArchive)
if dl.SizeBytes > 0 {
fmt.Printf("size hint: %d bytes\n", dl.SizeBytes)
}

out, err := os.Create(localFile)
if err != nil {
die(err)
}

// Read to EOF. The final Read verifies the integrity footer, so an error
// here means the transfer was truncated or corrupted.
n, copyErr := io.Copy(out, dl.Body)
closeErr := out.Close()

if copyErr != nil {
os.Remove(localFile) // discard corrupted output
die(fmt.Errorf("download failed after %d bytes (output discarded): %w", n, copyErr))
}
if closeErr != nil {
die(closeErr)
}

fmt.Printf("wrote: %d bytes to %s\n", n, localFile)
fmt.Println("checksum: verified")
}

func mustEnv(key string) string {
v := os.Getenv(key)
if v == "" {
fmt.Fprintf(os.Stderr, "%s is required\n", key)
os.Exit(2)
}
return v
}

func mustClient() *godo.Client {
token := mustEnv("DIGITALOCEAN_TOKEN")
client := godo.NewFromToken(token)
if baseURL := os.Getenv("DIGITALOCEAN_API_URL"); baseURL != "" {
u, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
client.BaseURL = u
}
return client
}

func die(err error) {
var apiErr *godo.ErrorResponse
if errors.As(err, &apiErr) {
fmt.Fprintf(os.Stderr, "API error (HTTP %d): %s\n", apiErr.Response.StatusCode, apiErr.Message)
} else {
fmt.Fprintln(os.Stderr, err)
}
os.Exit(1)
}
86 changes: 86 additions & 0 deletions examples/hosted-agents/exec-in-sandbox/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"context"
"errors"
"fmt"
"net/url"
"os"
"strings"

"github.com/digitalocean/godo"
)

func main() {
sessionID := os.Getenv("HOSTED_AGENT_SESSION_ID")
if sessionID == "" {
fmt.Fprintln(os.Stderr, "HOSTED_AGENT_SESSION_ID is required")
os.Exit(2)
}

raw := envOr("EXEC_COMMAND", "echo,hello from sandbox")
argv := strings.Split(raw, ",")
for i := range argv {
argv[i] = strings.TrimSpace(argv[i])
}

client := mustClient()
ctx := context.Background()

out, resp, err := client.HostedAgents.ExecInSandbox(ctx, sessionID, &godo.HostedAgentSandboxExecRequest{
Argv: argv,
Workdir: os.Getenv("EXEC_WORKDIR"),
TimeoutSeconds: 30,
})
if err != nil {
var apiErr *godo.ErrorResponse
if errors.As(err, &apiErr) && apiErr.Response.StatusCode == 501 {
fmt.Fprintf(os.Stderr, "HTTP 501 — ExecInSandbox is not implemented on this server yet\n")
os.Exit(1)
}
die(err)
}

fmt.Printf("HTTP %d\n", resp.StatusCode)
fmt.Printf("exit_code: %d\n", out.ExitCode)
if out.Stdout != "" {
fmt.Printf("stdout:\n%s\n", out.Stdout)
}
if out.Stderr != "" {
fmt.Printf("stderr:\n%s\n", out.Stderr)
}
}

func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

func mustClient() *godo.Client {
token := os.Getenv("DIGITALOCEAN_TOKEN")
if token == "" {
fmt.Fprintln(os.Stderr, "DIGITALOCEAN_TOKEN is required")
os.Exit(2)
}
client := godo.NewFromToken(token)
if baseURL := os.Getenv("DIGITALOCEAN_API_URL"); baseURL != "" {
u, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
client.BaseURL = u
}
return client
}

func die(err error) {
var apiErr *godo.ErrorResponse
if errors.As(err, &apiErr) {
fmt.Fprintf(os.Stderr, "API error (HTTP %d): %s\n", apiErr.Response.StatusCode, apiErr.Message)
} else {
fmt.Fprintln(os.Stderr, err)
}
os.Exit(1)
}
Loading
Loading