Skip to content
Draft
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
3 changes: 3 additions & 0 deletions internal/agent/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Agent struct {
RuntimeID string `json:"runtime_id,omitempty"`
RuntimeKind string `json:"runtime_kind,omitempty"`
Image string `json:"image,omitempty"`
Avatar string `json:"avatar,omitempty"`
BoxID string `json:"box_id,omitempty"`
RuntimeOptions map[string]any `json:"runtime_options,omitempty"`
Role string `json:"role"`
Expand All @@ -37,6 +38,7 @@ type CreateAgentSpec struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Image string `json:"image,omitempty"`
Avatar string `json:"avatar,omitempty"`
RuntimeKind string `json:"runtime_kind,omitempty"`
FromTemplate string `json:"from_template,omitempty"`
Role string `json:"role,omitempty"`
Expand All @@ -51,6 +53,7 @@ type UpdateRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Image *string `json:"image,omitempty"`
Avatar *string `json:"avatar,omitempty"`
RuntimeOptions *map[string]any `json:"runtime_options,omitempty"`
AgentProfile *AgentProfile `json:"agent_profile,omitempty"`
}
Expand Down
115 changes: 99 additions & 16 deletions internal/agent/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package agent
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -431,6 +432,19 @@ func (s *Service) ensureManager(ctx context.Context, forceRecreate bool, imageOv
if s == nil {
return Agent{}, fmt.Errorf("agent service is required")
}
managerAvatar := ""
s.mu.RLock()
if existing, ok := s.agents[ManagerUserID]; ok {
managerAvatar = strings.TrimSpace(existing.Avatar)
} else {
for _, existing := range s.agents {
if isManagerAgent(existing) {
managerAvatar = strings.TrimSpace(existing.Avatar)
break
}
}
}
s.mu.RUnlock()
runtimeKind := runtimeKindForGatewayRuntime(runtimeOverride)
if strings.TrimSpace(runtimeOverride) != "" && runtimeKind == "" {
return Agent{}, fmt.Errorf("gateway runtime %q is not supported", runtimeOverride)
Expand Down Expand Up @@ -522,6 +536,7 @@ func (s *Service) ensureManager(ctx context.Context, forceRecreate bool, imageOv
RuntimeID: runtimeIDForAgentID(ManagerUserID),
RuntimeKind: runtimeKind,
Image: managerImage,
Avatar: managerAvatar,
Status: "profile_incomplete",
CreatedAt: now,
Role: RoleManager,
Expand Down Expand Up @@ -600,6 +615,7 @@ func (s *Service) ensureManager(ctx context.Context, forceRecreate bool, imageOv
RuntimeID: runtimeIDForAgentID(ManagerUserID),
RuntimeKind: s.gatewayRuntimeKind(),
Image: managerImage,
Avatar: managerAvatar,
BoxID: info.ID,
Status: string(info.State),
CreatedAt: info.CreatedAt.UTC(),
Expand Down Expand Up @@ -913,6 +929,9 @@ func (s *Service) replace(ctx context.Context, req CreateRequest) (Agent, error)
if strings.TrimSpace(spec.Image) == "" {
spec.Image = existing.Image
}
if strings.TrimSpace(spec.Avatar) == "" {
spec.Avatar = existing.Avatar
}
if strings.TrimSpace(spec.RuntimeKind) == "" {
spec.RuntimeKind = existing.RuntimeKind
}
Expand Down Expand Up @@ -956,6 +975,7 @@ func mergeReplaceSpec(existing Agent, next CreateAgentSpec, fieldMask []string)
Name: existing.Name,
Description: existing.Description,
Image: existing.Image,
Avatar: existing.Avatar,
RuntimeKind: existing.RuntimeKind,
Role: existing.Role,
Status: existing.Status,
Expand All @@ -977,6 +997,8 @@ func mergeReplaceSpec(existing Agent, next CreateAgentSpec, fieldMask []string)
merged.Description = next.Description
case "image":
merged.Image = next.Image
case "avatar":
merged.Avatar = next.Avatar
case "runtime_kind":
merged.RuntimeKind = next.RuntimeKind
case "role":
Expand Down Expand Up @@ -1045,7 +1067,7 @@ func (s *Service) agentSnapshot(id string) (Agent, bool) {
}

func (s *Service) resolveAgentBox(ctx context.Context, rt sandbox.Runtime, got Agent) (sandbox.Instance, string, error) {
keys := make([]string, 0, 2)
keys := make([]string, 0, 3)
if boxID := strings.TrimSpace(got.BoxID); boxID != "" {
keys = append(keys, boxID)
}
Expand All @@ -1054,6 +1076,18 @@ func (s *Service) resolveAgentBox(ctx context.Context, rt sandbox.Runtime, got A
keys = append(keys, name)
}
}
if runtimeName := safeSandboxNameForAgent(got.ID, got.Name); runtimeName != "" {
duplicate := false
for _, key := range keys {
if key == runtimeName {
duplicate = true
break
}
}
if !duplicate {
keys = append(keys, runtimeName)
}
}
if len(keys) == 0 {
return nil, "", fmt.Errorf("agent box identifier is required")
}
Expand Down Expand Up @@ -1211,11 +1245,12 @@ func (s *Service) Delete(ctx context.Context, id string) error {
return fmt.Errorf("remove agent box: %w", err)
}
} else {
rt, ensureErr := s.ensureRuntime(existing.Name)
runtimeName := sandboxNameForAgent(existing)
rt, ensureErr := s.ensureRuntime(runtimeName)
if ensureErr != nil {
return ensureErr
}
runtimeHome, homeErr := s.sandboxRuntimeHome(existing.Name)
runtimeHome, homeErr := s.sandboxRuntimeHome(runtimeName)
if homeErr != nil {
return homeErr
}
Expand Down Expand Up @@ -1251,7 +1286,7 @@ func (s *Service) Delete(ctx context.Context, id string) error {
}
delete(s.agents, id)
s.deleteRuntimeRecordLocked(current.RuntimeID)
runtimeHome, err := s.sandboxRuntimeHome(current.Name)
runtimeHome, err := s.sandboxRuntimeHome(sandboxNameForAgent(current))
if err != nil {
s.mu.Unlock()
return err
Expand Down Expand Up @@ -1297,6 +1332,48 @@ func isRetryableRemoveAllError(err error) bool {
return errors.Is(err, syscall.ENOTEMPTY) || strings.Contains(strings.ToLower(err.Error()), "directory not empty")
}

func safeSandboxNameForAgent(id, name string) string {
name = strings.TrimSpace(name)
if isDockerSafeSandboxName(name) {
return name
}
id = strings.TrimSpace(id)
if isDockerSafeSandboxName(id) {
return id
}
seed := id
if seed == "" {
seed = name
}
if seed == "" {
seed = "agent"
}
sum := sha256.Sum256([]byte(seed))
return fmt.Sprintf("agent-%x", sum[:8])
}

func sandboxNameForAgent(got Agent) string {
return safeSandboxNameForAgent(got.ID, got.Name)
}

func isDockerSafeSandboxName(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return false
}
for idx := 0; idx < len(name); idx++ {
ch := name[idx]
valid := (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')
if idx > 0 {
valid = valid || ch == '_' || ch == '.' || ch == '-'
}
if !valid {
return false
}
}
return true
}

func (s *Service) List() []Agent {
s.mu.RLock()
agents := sortedAgentsFromMap(s.agents)
Expand Down Expand Up @@ -1371,6 +1448,7 @@ func (s *Service) CreateWorker(ctx context.Context, spec CreateAgentSpec) (Agent
name := strings.TrimSpace(spec.Name)
description := strings.TrimSpace(spec.Description)
image := strings.TrimSpace(spec.Image)
avatar := strings.TrimSpace(spec.Avatar)
runtimeKind := strings.TrimSpace(spec.RuntimeKind)
switch {
case name == "":
Expand Down Expand Up @@ -1414,6 +1492,10 @@ func (s *Service) CreateWorker(ctx context.Context, spec CreateAgentSpec) (Agent
if err := s.ensureCodexResponsesAPI(ctx, runtimeKind, resolvedProfile); err != nil {
return Agent{}, err
}
runtimeAgentName := name
if isGatewayRuntimeKind(runtimeKind) {
runtimeAgentName = safeSandboxNameForAgent(id, name)
}
runtimeProfile := s.runtimeProfileForKind(runtimeKind, id, name, description, resolvedProfile)
if err := s.provisionRuntime(ctx, runtimeImpl, runtimeKind, agentruntime.ProvisionRequest{
RuntimeID: runtimeIDForAgentID(id),
Expand All @@ -1425,32 +1507,32 @@ func (s *Service) CreateWorker(ctx context.Context, spec CreateAgentSpec) (Agent
return Agent{}, fmt.Errorf("provision worker runtime: %w", err)
}
if testCreateGatewayBoxHook != nil && isGatewayRuntimeKind(runtimeKind) {
rt, err := s.ensureRuntime(name)
rt, err := s.ensureRuntime(runtimeAgentName)
if err != nil {
return Agent{}, err
}
runtimeHome, err := s.sandboxRuntimeHome(name)
runtimeHome, err := s.sandboxRuntimeHome(runtimeAgentName)
if err != nil {
return Agent{}, err
}
defer func() {
_ = s.closeRuntime(runtimeHome, rt)
}()
box, info, err := s.createGatewayBox(ctx, rt, image, name, id, resolvedProfile)
box, info, err := s.createGatewayBox(ctx, rt, image, runtimeAgentName, id, resolvedProfile)
if err != nil {
return Agent{}, fmt.Errorf("create worker box: %w", err)
}
defer func() {
_ = s.closeBox(box)
}()
return s.persistCreatedWorker(ctx, id, name, description, image, runtimeKind, resolvedProfile, spec.RuntimeOptions, agentruntime.Info{
return s.persistCreatedWorker(ctx, id, name, description, image, avatar, runtimeKind, resolvedProfile, spec.RuntimeOptions, agentruntime.Info{
HandleID: strings.TrimSpace(info.ID),
State: agentruntime.State(info.State),
CreatedAt: info.CreatedAt.UTC(),
})
}
if runtimeKind == RuntimeKindCodex {
if err := s.persistStartingWorker(ctx, id, name, description, image, runtimeKind, resolvedProfile, spec.RuntimeOptions); err != nil {
if err := s.persistStartingWorker(ctx, id, name, description, image, avatar, runtimeKind, resolvedProfile, spec.RuntimeOptions); err != nil {
return Agent{}, err
}
defer func() {
Expand All @@ -1462,7 +1544,7 @@ func (s *Service) CreateWorker(ctx context.Context, spec CreateAgentSpec) (Agent
handle, err := runtimeImpl.New(ctx, agentruntime.Spec{
RuntimeID: runtimeIDForAgentID(id),
AgentID: id,
AgentName: name,
AgentName: runtimeAgentName,
Image: image,
Profile: runtimeProfile,
})
Expand All @@ -1475,10 +1557,10 @@ func (s *Service) CreateWorker(ctx context.Context, spec CreateAgentSpec) (Agent
CreatedAt: time.Now().UTC(),
}

return s.persistCreatedWorker(ctx, id, name, description, image, runtimeKind, resolvedProfile, spec.RuntimeOptions, info)
return s.persistCreatedWorker(ctx, id, name, description, image, avatar, runtimeKind, resolvedProfile, spec.RuntimeOptions, info)
}

func (s *Service) persistStartingWorker(ctx context.Context, id, name, description, image, runtimeKind string, profile AgentProfile, runtimeOptions map[string]any) error {
func (s *Service) persistStartingWorker(ctx context.Context, id, name, description, image, avatar, runtimeKind string, profile AgentProfile, runtimeOptions map[string]any) error {
s.mu.Lock()

if _, ok := s.agents[id]; ok {
Expand All @@ -1490,7 +1572,7 @@ func (s *Service) persistStartingWorker(ctx context.Context, id, name, descripti
return fmt.Errorf("agent name %q already exists", name)
}

worker := newWorkerAgent(id, name, description, image, runtimeKind, profile, runtimeOptions, agentruntime.Info{
worker := newWorkerAgent(id, name, description, image, avatar, runtimeKind, profile, runtimeOptions, agentruntime.Info{
State: agentruntime.StateCreated,
CreatedAt: time.Now().UTC(),
})
Expand All @@ -1517,7 +1599,7 @@ func (s *Service) removeStartingWorker(ctx context.Context, id string) error {
return err
}

func (s *Service) persistCreatedWorker(ctx context.Context, id, name, description, image, runtimeKind string, profile AgentProfile, createRuntimeExt map[string]any, info agentruntime.Info) (Agent, error) {
func (s *Service) persistCreatedWorker(ctx context.Context, id, name, description, image, avatar, runtimeKind string, profile AgentProfile, createRuntimeExt map[string]any, info agentruntime.Info) (Agent, error) {
s.mu.Lock()

if existing, ok := s.agents[id]; ok && !isStartingWorker(existing) {
Expand All @@ -1529,7 +1611,7 @@ func (s *Service) persistCreatedWorker(ctx context.Context, id, name, descriptio
return Agent{}, fmt.Errorf("agent name %q already exists", name)
}

worker := newWorkerAgent(id, name, description, image, runtimeKind, profile, createRuntimeExt, info)
worker := newWorkerAgent(id, name, description, image, avatar, runtimeKind, profile, createRuntimeExt, info)
s.agents[worker.ID] = worker
s.syncRuntimeRecordLocked(worker)
if worker.AgentProfile.ProfileComplete {
Expand All @@ -1549,7 +1631,7 @@ func (s *Service) persistCreatedWorker(ctx context.Context, id, name, descriptio
return created, nil
}

func newWorkerAgent(id, name, description, image, runtimeKind string, profile AgentProfile, runtimeOptions map[string]any, info agentruntime.Info) Agent {
func newWorkerAgent(id, name, description, image, avatar, runtimeKind string, profile AgentProfile, runtimeOptions map[string]any, info agentruntime.Info) Agent {
createdAt := info.CreatedAt.UTC()
if info.CreatedAt.IsZero() {
createdAt = time.Now().UTC()
Expand All @@ -1569,6 +1651,7 @@ func newWorkerAgent(id, name, description, image, runtimeKind string, profile Ag
RuntimeID: runtimeIDForAgentID(id),
RuntimeKind: runtimeKind,
Image: image,
Avatar: strings.TrimSpace(avatar),
BoxID: strings.TrimSpace(info.HandleID),
Description: description,
Status: string(state),
Expand Down
18 changes: 13 additions & 5 deletions internal/agent/service_profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ func (s *Service) Update(ctx context.Context, id string, req UpdateRequest) (Age
if req.Image != nil {
current.Image = strings.TrimSpace(*req.Image)
}
if req.Avatar != nil {
current.Avatar = strings.TrimSpace(*req.Avatar)
}
if req.AgentProfile != nil || req.RuntimeOptions != nil {
profileUpdated = true
profile := current.AgentProfile
Expand Down Expand Up @@ -323,11 +326,12 @@ func (s *Service) Recreate(ctx context.Context, id string) (Agent, error) {
}

if testCreateGatewayBoxHook != nil {
rt, err := s.ensureRuntime(got.Name)
runtimeName := sandboxNameForAgent(got)
rt, err := s.ensureRuntime(runtimeName)
if err != nil {
return Agent{}, err
}
runtimeHome, err := s.sandboxRuntimeHome(got.Name)
runtimeHome, err := s.sandboxRuntimeHome(runtimeName)
if err != nil {
return Agent{}, err
}
Expand All @@ -340,7 +344,7 @@ func (s *Service) Recreate(ctx context.Context, id string) (Agent, error) {
return Agent{}, fmt.Errorf("remove existing agent box: %w", deleteErr)
}
}
box, sandboxInfo, err := s.createGatewayBox(ctx, rt, image, got.Name, got.ID, profile)
box, sandboxInfo, err := s.createGatewayBox(ctx, rt, image, runtimeName, got.ID, profile)
if err != nil {
return Agent{}, fmt.Errorf("create agent box: %w", err)
}
Expand All @@ -366,17 +370,21 @@ func (s *Service) Recreate(ctx context.Context, id string) (Agent, error) {
}

runtimeProfile := s.runtimeProfileForKind(runtimeKind, got.ID, got.Name, got.Description, profile)
runtimeAgentName := got.Name
if isGatewayRuntimeKind(runtimeKind) {
runtimeAgentName = sandboxNameForAgent(got)
}
createSpec := agentruntime.Spec{
RuntimeID: normalizeRuntimeID(got.RuntimeID, got.ID),
AgentID: got.ID,
AgentName: got.Name,
AgentName: runtimeAgentName,
Image: image,
Profile: runtimeProfile,
}
if err := s.provisionRuntime(ctx, runtimeImpl, runtimeKind, agentruntime.ProvisionRequest{
RuntimeID: createSpec.RuntimeID,
AgentID: createSpec.AgentID,
AgentName: createSpec.AgentName,
AgentName: got.Name,
Profile: runtimeProfile,
}); err != nil {
return Agent{}, fmt.Errorf("provision agent runtime: %w", err)
Expand Down
Loading