Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 2 additions & 3 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,9 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command {
providers: registered,
noTTY: true,
retrySleep: 5 * time.Second,
sandboxCmd: []string{"bash", "-c",
". /sandbox/.config/openshell/sandbox.env 2>/dev/null && " +
"cat /sandbox/.config/openshell/sandbox.env >> /sandbox/.bashrc 2>/dev/null; true"},
sandboxCmd: []string{"true"},
payloadDir: payloadDir,
env: agentCfg.BuildEnvMap(),
onSuccess: func(n string) {
fmt.Println()
status.OKf("Sandbox created: %s — connect with: harness connect %s", n, n)
Expand Down
2 changes: 1 addition & 1 deletion cmd/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (m *mockGW) SandboxDelete(name string) error {
m.deletedNames = append(m.deletedNames, name)
return nil
}
func (m *mockGW) CLIVersion() string { return "openshell v0.0.58" }
func (m *mockGW) CLIVersion() string { return "openshell v0.0.59" }
func (m *mockGW) CLIPath() string { return "/usr/bin/openshell" }
func (m *mockGW) InferenceModel() string { return "" }
func (m *mockGW) InferenceSet(string, string) error { return nil }
Expand Down
2 changes: 2 additions & 0 deletions cmd/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type sandboxOpts struct {
retrySleep time.Duration // pause between retry attempts
sandboxCmd []string // command to run inside the sandbox
payloadDir string // pre-rendered payload dir to upload
env map[string]string // env vars injected via --env on sandbox create
onSuccess func(name string) // called after successful creation (optional)
}

Expand Down Expand Up @@ -64,6 +65,7 @@ func createSandbox(opts sandboxOpts) error {
UploadSrc: uploadDir,
UploadDst: "/sandbox/.config",
Command: opts.sandboxCmd,
Env: opts.env,
})
if err == nil {
if opts.onSuccess != nil {
Expand Down
10 changes: 6 additions & 4 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func NewUpCmd(harnessDir, cli string) *cobra.Command {
agentPath := resolveAgentPath(harnessDir, agentName, agentFile)

gw := gateway.New(cli)
if err := gw.CheckMinVersion("0.0.59"); err != nil {
status.Warn(fmt.Sprintf("OpenShell version: %v", err))
}

gwName := "local"
if remote {
Expand Down Expand Up @@ -187,13 +190,11 @@ func upLocal(opts upLocalOpts) error {

// 5. Create sandbox
status.Header("Sandbox")
envInit := ". /sandbox/.config/openshell/sandbox.env 2>/dev/null && " +
"cat /sandbox/.config/openshell/sandbox.env >> /sandbox/.bashrc 2>/dev/null; "
var sandboxCmd []string
if noTTY {
sandboxCmd = []string{"bash", "-c", envInit + "true"}
sandboxCmd = []string{"true"}
} else {
sandboxCmd = []string{"bash", "-c", envInit + "exec bash /sandbox/.config/openshell/run.sh"}
sandboxCmd = []string{"bash", "/sandbox/.config/openshell/run.sh"}
}

return createSandbox(sandboxOpts{
Expand All @@ -206,6 +207,7 @@ func upLocal(opts upLocalOpts) error {
retrySleep: opts.retrySleep,
sandboxCmd: sandboxCmd,
payloadDir: payloadDir,
env: agentCfg.BuildEnvMap(),
})
}

Expand Down
28 changes: 9 additions & 19 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,21 @@ func Parse(data []byte) (*AgentConfig, error) {
return &cfg, nil
}

func (c *AgentConfig) BuildEnvSh() string {
func (c *AgentConfig) BuildEnvMap() map[string]string {
env := make(map[string]string)
for k, v := range c.Env {
env[k] = v
env[k] = os.ExpandEnv(v)
}
for _, p := range c.Providers {
for k, v := range p.Config {
env[k] = v
env[k] = os.ExpandEnv(v)
}
}
return env
}

func (c *AgentConfig) BuildEnvSh() string {
env := c.BuildEnvMap()
if len(env) == 0 {
return ""
}
Expand All @@ -93,7 +98,7 @@ func (c *AgentConfig) BuildEnvSh() string {
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
fmt.Fprintf(&b, "export %s=%q\n", k, os.ExpandEnv(env[k]))
fmt.Fprintf(&b, "export %s=%q\n", k, env[k])
}
return b.String()
}
Expand All @@ -102,10 +107,6 @@ func (c *AgentConfig) BuildRunSh() string {
var b strings.Builder
b.WriteString("#!/usr/bin/env bash\nset -euo pipefail\n\n")
b.WriteString("PAYLOAD_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n\n")
b.WriteString("# Source environment\n")
b.WriteString("if [[ -f \"$PAYLOAD_DIR/env.sh\" ]]; then\n")
b.WriteString(" . \"$PAYLOAD_DIR/env.sh\"\n")
b.WriteString("fi\n\n")
b.WriteString("# Prepend payload bin to PATH\n")
b.WriteString("export PATH=\"$PAYLOAD_DIR/bin:$PATH\"\n\n")
b.WriteString("# Git auth\n")
Expand Down Expand Up @@ -135,17 +136,6 @@ func RenderPayload(cfg *AgentConfig, baseDir, destDir string) error {
return fmt.Errorf("creating bin dir: %w", err)
}

if envContent := cfg.BuildEnvSh(); envContent != "" {
if err := os.WriteFile(filepath.Join(destDir, "env.sh"), []byte(envContent), 0o644); err != nil {
return fmt.Errorf("writing env.sh: %w", err)
}
// sandbox.env is sourced by the sandbox image's startup.sh on boot,
// making env vars available to sandbox exec sessions.
if err := os.WriteFile(filepath.Join(destDir, "sandbox.env"), []byte(envContent), 0o644); err != nil {
return fmt.Errorf("writing sandbox.env: %w", err)
}
}

runSh := cfg.BuildRunSh()
if err := os.WriteFile(filepath.Join(destDir, "run.sh"), []byte(runSh), 0o755); err != nil {
return fmt.Errorf("writing run.sh: %w", err)
Expand Down
46 changes: 37 additions & 9 deletions internal/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,39 @@ env:
}
}

func TestBuildEnvMap(t *testing.T) {
cfg := &AgentConfig{
Env: map[string]string{
"TOP_VAR": "top-val",
"SHARED": "from-top",
},
Providers: []ProviderRef{
{Profile: "atlassian", Config: map[string]string{
"JIRA_URL": "https://issues.redhat.com",
"SHARED": "from-provider",
}},
},
}
env := cfg.BuildEnvMap()
if env["TOP_VAR"] != "top-val" {
t.Errorf("TOP_VAR = %q, want top-val", env["TOP_VAR"])
}
if env["JIRA_URL"] != "https://issues.redhat.com" {
t.Errorf("JIRA_URL = %q", env["JIRA_URL"])
}
if env["SHARED"] != "from-provider" {
t.Errorf("SHARED = %q, want from-provider (provider should override top-level)", env["SHARED"])
}
}

func TestBuildEnvMap_Empty(t *testing.T) {
cfg := &AgentConfig{Providers: []ProviderRef{{Profile: "github"}}}
env := cfg.BuildEnvMap()
if len(env) != 0 {
t.Errorf("expected empty map, got %v", env)
}
}

func TestBuildEnvSh_Sorted(t *testing.T) {
cfg := &AgentConfig{
Providers: []ProviderRef{
Expand All @@ -253,8 +286,8 @@ func TestBuildRunSh(t *testing.T) {
if !strings.Contains(runSh, "#!/usr/bin/env bash") {
t.Error("missing shebang")
}
if !strings.Contains(runSh, ". \"$PAYLOAD_DIR/env.sh\"") {
t.Error("missing env.sh source")
if strings.Contains(runSh, "env.sh") {
t.Error("run.sh should not source env.sh — env vars are injected via --env")
}
if !strings.Contains(runSh, "gh auth setup-git") {
t.Error("missing gh auth setup-git")
Expand Down Expand Up @@ -296,9 +329,6 @@ func TestRenderPayload(t *testing.T) {
t.Fatalf("RenderPayload: %v", err)
}

if _, err := os.Stat(filepath.Join(destDir, "env.sh")); err != nil {
t.Error("missing env.sh")
}
if _, err := os.Stat(filepath.Join(destDir, "run.sh")); err != nil {
t.Error("missing run.sh")
}
Expand All @@ -308,10 +338,8 @@ func TestRenderPayload(t *testing.T) {
if _, err := os.Stat(filepath.Join(destDir, "bin")); err != nil {
t.Error("missing bin/ directory")
}

envData, _ := os.ReadFile(filepath.Join(destDir, "env.sh"))
if !strings.Contains(string(envData), "JIRA_URL") {
t.Errorf("env.sh missing JIRA_URL:\n%s", envData)
if _, err := os.Stat(filepath.Join(destDir, "env.sh")); !os.IsNotExist(err) {
t.Error("env.sh should not be created — env vars are injected via --env")
}

runData, _ := os.ReadFile(filepath.Join(destDir, "run.sh"))
Expand Down
62 changes: 62 additions & 0 deletions internal/gateway/cli.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package gateway

import (
"fmt"
"io"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"
"syscall"

Expand Down Expand Up @@ -38,6 +41,55 @@ func (c *CLI) CLIPath() string {
return path
}

func ParseCLIVersion(raw string) string {
raw = strings.TrimSpace(raw)
if i := strings.LastIndex(raw, "v"); i >= 0 {
return raw[i+1:]
}
return raw
}

func parseVersionParts(v string) (major, minor, patch int, ok bool) {
parts := strings.SplitN(v, ".", 3)
if len(parts) != 3 {
return 0, 0, 0, false
}
var err error
major, err = strconv.Atoi(parts[0])
if err != nil {
return 0, 0, 0, false
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return 0, 0, 0, false
}
patch, err = strconv.Atoi(parts[2])
if err != nil {
return 0, 0, 0, false
}
return major, minor, patch, true
}

func (c *CLI) CheckMinVersion(minVersion string) error {
raw := c.CLIVersion()
if raw == "" {
return fmt.Errorf("could not determine openshell version")
}
installed := ParseCLIVersion(raw)
iMaj, iMin, iPatch, ok := parseVersionParts(installed)
if !ok {
return fmt.Errorf("could not parse openshell version %q", installed)
}
mMaj, mMin, mPatch, ok := parseVersionParts(minVersion)
if !ok {
return fmt.Errorf("invalid minimum version %q", minVersion)
}
if iMaj < mMaj || (iMaj == mMaj && iMin < mMin) || (iMaj == mMaj && iMin == mMin && iPatch < mPatch) {
return fmt.Errorf("openshell %s is below minimum %s (upgrade: openshell update)", installed, minVersion)
}
return nil
}

func (c *CLI) InferenceGet() error {
return c.silent("inference", "get")
}
Expand Down Expand Up @@ -211,6 +263,16 @@ func (c *CLI) SandboxCreate(opts SandboxCreateOpts) error {
if opts.UploadSrc != "" {
args = append(args, "--upload", opts.UploadSrc+":"+opts.UploadDst, "--no-git-ignore")
}
if len(opts.Env) > 0 {
keys := make([]string, 0, len(opts.Env))
for k := range opts.Env {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
args = append(args, "--env", k+"="+opts.Env[k])
}
}
if len(opts.Command) > 0 {
args = append(args, "--")
args = append(args, opts.Command...)
Expand Down
Loading
Loading