un-inception/clients/go/sync/src/client.go
russell@unturf.com ed6c52b001 Fix --account N credential priority across all SDKs
--account N was silently ignored when UNSANDBOX_PUBLIC_KEY/SECRET_KEY
env vars were set. The credential resolution checked env vars at tier 2
before ever reaching the CSV lookup, so the explicit flag had no effect.

Correct priority order (all 8 SDKs):
  1. CLI -p/-k flags (explicit key args)
  2. --account N → direct CSV row lookup (bypasses env vars)
  3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
  4. ~/.unsandbox/accounts.csv default (row 0 or UNSANDBOX_ACCOUNT)

SDKs updated: C, Python, Go, JavaScript, Ruby, PHP, Java, Rust

Also adds:
- --account N flag to CLI parsers in all 8 SDKs (Go, PHP, Java, Rust
  previously had no flag at all; Python/JS/Ruby had the parameter but
  never wired it to the CLI)
- test_account_flag.sh integration test for each SDK verifying the
  priority behavior with real and garbage credentials
- test-integration Makefile target for the C SDK
2026-03-23 14:56:00 -04:00

200 lines
5.2 KiB
Go

// Client provides a struct-based API around the function-based SDK.
// Designed for consumers like orchestra that prefer method receivers
// and context-aware patterns.
package un
import (
"context"
"fmt"
)
// Client wraps resolved credentials for method-based API access.
type Client struct {
Creds *Credentials
}
// NewClient creates a client from explicit credentials.
func NewClient(publicKey, secretKey string) *Client {
return &Client{Creds: &Credentials{PublicKey: publicKey, SecretKey: secretKey}}
}
// NewClientFromEnv resolves credentials from the 4-tier priority system.
func NewClientFromEnv() (*Client, error) {
creds, err := ResolveCredentials("", "", -1)
if err != nil {
return nil, err
}
return &Client{Creds: creds}, nil
}
// ExecuteResult holds typed execution output.
type ExecuteResult struct {
JobID string
Status string
Output string
Error string
Raw map[string]interface{}
}
func toExecuteResult(raw map[string]interface{}) *ExecuteResult {
r := &ExecuteResult{Raw: raw}
if v, ok := raw["job_id"].(string); ok {
r.JobID = v
}
if v, ok := raw["status"].(string); ok {
r.Status = v
}
// unsandbox API returns stdout/stderr, not output/error
if v, ok := raw["output"].(string); ok {
r.Output = v
}
if v, ok := raw["stdout"].(string); ok && r.Output == "" {
r.Output = v
}
if v, ok := raw["error"].(string); ok {
r.Error = v
}
if v, ok := raw["stderr"].(string); ok && r.Error == "" {
r.Error = v
}
return r
}
// Execute runs code synchronously (blocks until completion).
func (c *Client) Execute(_ context.Context, language, code string) (*ExecuteResult, error) {
return c.ExecuteWithOpts(context.Background(), language, code, "")
}
// ExecuteWithOpts runs code with optional network mode.
func (c *Client) ExecuteWithOpts(_ context.Context, language, code, networkMode string) (*ExecuteResult, error) {
// Use the low-level makeRequest to support network_mode
data := map[string]interface{}{
"language": language,
"code": code,
}
if networkMode != "" {
data["network_mode"] = networkMode
}
resp, err := makeRequest("POST", "/execute", c.Creds, data)
if err != nil {
return nil, err
}
result := toExecuteResult(resp)
// Poll if job is not terminal
if result.JobID != "" && result.Status != "completed" && result.Status != "failed" {
polled, err := WaitForJob(c.Creds, result.JobID)
if err != nil {
return nil, err
}
return toExecuteResult(polled), nil
}
return result, nil
}
// WaitForJobResult polls a job until completion.
func (c *Client) WaitForJobResult(_ context.Context, jobID string) (*ExecuteResult, error) {
raw, err := WaitForJob(c.Creds, jobID)
if err != nil {
return nil, err
}
return toExecuteResult(raw), nil
}
// SessionResult holds typed session info.
type SessionResult struct {
ID string
Status string
Raw map[string]interface{}
}
// CreateSession creates a new execution session.
func (c *Client) CreateSession(_ context.Context, language, networkMode string) (*SessionResult, error) {
opts := &SessionOptions{
Shell: language,
}
if networkMode != "" {
opts.NetworkMode = networkMode
}
raw, err := CreateSession(c.Creds, opts)
if err != nil {
return nil, err
}
s := &SessionResult{Raw: raw}
if v, ok := raw["session_id"].(string); ok {
s.ID = v
}
if v, ok := raw["id"].(string); ok && s.ID == "" {
s.ID = v
}
if v, ok := raw["status"].(string); ok {
s.Status = v
}
return s, nil
}
// ShellExec runs a command in an existing session.
func (c *Client) ShellExec(_ context.Context, sessionID, command string) (*ExecuteResult, error) {
raw, err := ShellSession(c.Creds, sessionID, command)
if err != nil {
return nil, err
}
result := toExecuteResult(raw)
// Poll if async
if result.JobID != "" && result.Status != "completed" && result.Status != "failed" {
polled, err := WaitForJob(c.Creds, result.JobID)
if err != nil {
return nil, err
}
return toExecuteResult(polled), nil
}
return result, nil
}
// DestroySession deletes a session.
func (c *Client) DestroySession(_ context.Context, sessionID string) error {
_, err := DeleteSession(c.Creds, sessionID)
return err
}
// Sessions returns all active sessions.
func (c *Client) Sessions(_ context.Context) ([]map[string]interface{}, error) {
return ListSessions(c.Creds)
}
// Services returns all services.
func (c *Client) Services(_ context.Context) ([]map[string]interface{}, error) {
return ListServices(c.Creds)
}
// ServiceLogs retrieves logs for a service.
func (c *Client) ServiceLogs(_ context.Context, serviceID string) (string, error) {
raw, err := GetServiceLogs(c.Creds, serviceID, false)
if err != nil {
return "", err
}
if v, ok := raw["logs"].(string); ok {
return v, nil
}
return "", nil
}
// CheckKeys verifies credentials are valid and returns key info.
func (c *Client) CheckKeys(_ context.Context) (map[string]interface{}, error) {
return ValidateKeys(c.Creds)
}
// InjectDirectory uploads a local directory to a session as a tarball.
// This is a higher-level operation that creates a tar.gz, base64-chunks it,
// and streams it into the session via shell commands.
func (c *Client) InjectDirectory(_ context.Context, sessionID, localDir, remoteDir string) error {
return fmt.Errorf("InjectDirectory not implemented in SDK client — use orchestra's upload package")
}