576 lines
18 KiB
Go
576 lines
18 KiB
Go
package agents
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/office/models"
|
|
"github.com/kandev/kandev/internal/office/routing"
|
|
"github.com/kandev/kandev/internal/office/shared"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
ctxKeyAgentClaims = "agent_claims"
|
|
ctxKeyAgentCaller = "agent_caller"
|
|
)
|
|
|
|
// Handler provides HTTP handlers for agent routes.
|
|
type Handler struct {
|
|
svc *AgentService
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// RegisterRoutes registers agent and instruction HTTP routes on the given router group.
|
|
func RegisterRoutes(group *gin.RouterGroup, svc *AgentService, log *logger.Logger) {
|
|
h := &Handler{svc: svc, logger: log.WithFields(zap.String("component", "office-agents-handler"))}
|
|
|
|
group.GET("/workspaces/:wsId/agents", h.listAgents)
|
|
group.POST("/workspaces/:wsId/agents", h.createAgent)
|
|
group.GET("/agents/:id", h.getAgent)
|
|
group.PATCH("/agents/:id", h.updateAgent)
|
|
group.PATCH("/agents/:id/status", h.updateAgentStatus)
|
|
group.DELETE("/agents/:id", h.deleteAgent)
|
|
|
|
group.GET("/agents/:id/instructions", h.listInstructions)
|
|
group.GET("/agents/:id/instructions/:filename", h.getInstruction)
|
|
group.PUT("/agents/:id/instructions/:filename", h.upsertInstruction)
|
|
group.DELETE("/agents/:id/instructions/:filename", h.deleteInstruction)
|
|
|
|
group.GET("/agents/:id/utilization", h.getAgentUtilization)
|
|
|
|
group.GET("/agents/:id/memory", h.listMemory)
|
|
group.PUT("/agents/:id/memory", h.upsertMemory)
|
|
group.DELETE("/agents/:id/memory/all", h.deleteAllMemory)
|
|
group.DELETE("/agents/:id/memory/:entryId", h.deleteMemory)
|
|
group.GET("/agents/:id/memory/summary", h.memorySummary)
|
|
group.GET("/agents/:id/memory/export", h.exportMemory)
|
|
}
|
|
|
|
// AgentAuthMiddleware returns a gin.HandlerFunc that extracts and validates agent JWTs.
|
|
// Requests without a JWT are treated as UI/admin requests and pass through.
|
|
func AgentAuthMiddleware(svc *AgentService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
auth := c.GetHeader("Authorization")
|
|
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
|
c.Next()
|
|
return
|
|
}
|
|
token := strings.TrimPrefix(auth, "Bearer ")
|
|
claims, err := svc.ValidateAgentJWT(token)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
|
return
|
|
}
|
|
agent, err := svc.GetAgentInstance(c.Request.Context(), claims.AgentProfileID)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "agent not found"})
|
|
return
|
|
}
|
|
if wsID := c.Param("wsId"); wsID != "" && claims.WorkspaceID != wsID {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "token workspace mismatch"})
|
|
return
|
|
}
|
|
c.Set(ctxKeyAgentClaims, claims)
|
|
c.Set(ctxKeyAgentCaller, agent)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// agentCallerFromCtx returns the authenticated agent or nil for UI requests.
|
|
func agentCallerFromCtx(c *gin.Context) *models.AgentInstance {
|
|
val, ok := c.Get(ctxKeyAgentCaller)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
agent, _ := val.(*models.AgentInstance)
|
|
return agent
|
|
}
|
|
|
|
// CallerFromContext exposes the authenticated agent (or nil for UI
|
|
// requests) to other office packages that need to enforce agent-scoped
|
|
// authorization without depending on the agents package's internal
|
|
// context-key constants.
|
|
func CallerFromContext(c *gin.Context) *models.AgentInstance {
|
|
return agentCallerFromCtx(c)
|
|
}
|
|
|
|
// -- Agent handlers --
|
|
|
|
func (h *Handler) listAgents(c *gin.Context) {
|
|
filter := AgentListFilter{
|
|
Role: c.Query("role"),
|
|
Status: c.Query("status"),
|
|
ReportsTo: c.Query("reports_to"),
|
|
}
|
|
var (
|
|
agents []*models.AgentInstance
|
|
err error
|
|
)
|
|
if filter.Role != "" || filter.Status != "" || filter.ReportsTo != "" {
|
|
agents, err = h.svc.ListAgentInstancesFiltered(c.Request.Context(), c.Param("wsId"), filter)
|
|
} else {
|
|
agents, err = h.svc.ListAgentsFromConfig(c.Request.Context(), c.Param("wsId"))
|
|
}
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, AgentListResponse{Agents: agents})
|
|
}
|
|
|
|
func (h *Handler) createAgent(c *gin.Context) {
|
|
if err := checkAgentPermission(c, shared.PermCanCreateAgents); err != nil {
|
|
return
|
|
}
|
|
var req CreateAgentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := checkNoEscalation(c, req.Permissions); err != nil {
|
|
return
|
|
}
|
|
// The office agent gets a fresh row in agent_profiles. The repo's
|
|
// CreateAgentInstance derives a default agent_id (CLI tool) from the
|
|
// workspace's existing agents when AgentID is empty.
|
|
agent := &models.AgentInstance{
|
|
WorkspaceID: c.Param("wsId"),
|
|
Name: req.Name,
|
|
Role: models.AgentRole(req.Role),
|
|
Icon: req.Icon,
|
|
Status: models.AgentStatusIdle,
|
|
ReportsTo: req.ReportsTo,
|
|
Permissions: req.Permissions,
|
|
BudgetMonthlyCents: req.BudgetMonthlyCents,
|
|
MaxConcurrentSessions: req.MaxConcurrentSessions,
|
|
DesiredSkills: req.DesiredSkills,
|
|
ExecutorPreference: req.ExecutorPreference,
|
|
}
|
|
if err := h.svc.CreateAgentInstanceWithCaller(c.Request.Context(), agent, agentCallerFromCtx(c), req.Reason); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, AgentResponse{Agent: agent})
|
|
}
|
|
|
|
func (h *Handler) getAgent(c *gin.Context) {
|
|
agent, err := h.svc.GetAgentFromConfig(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, AgentResponse{Agent: agent})
|
|
}
|
|
|
|
func (h *Handler) updateAgent(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
targetID := c.Param("id")
|
|
if caller := agentCallerFromCtx(c); caller != nil {
|
|
if caller.ID != targetID && !isAdminRole(caller.Role) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "only CEO or admin can update other agents"})
|
|
return
|
|
}
|
|
}
|
|
agent, err := h.svc.GetAgentFromConfig(ctx, targetID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
var req UpdateAgentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if req.Permissions != nil {
|
|
if err := checkNoEscalation(c, *req.Permissions); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if req.Routing != nil {
|
|
if err := h.applyRoutingOverride(c, agent, *req.Routing); err != nil {
|
|
return
|
|
}
|
|
}
|
|
applyAgentUpdates(agent, &req)
|
|
if err := h.svc.UpdateAgentInstance(ctx, agent); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, AgentResponse{Agent: agent})
|
|
}
|
|
|
|
// applyRoutingOverride validates the override blob and writes it onto
|
|
// the agent's Settings JSON. Writes a structured 400 with per-field
|
|
// details on validation failure (and returns the error so the caller
|
|
// skips the persistence step).
|
|
//
|
|
// In addition to the catalogue-only checks (known/dup/tier shape), the
|
|
// agent's chosen tier override is checked against the workspace routing
|
|
// config: rejecting a tier that no provider has mapped at save time
|
|
// prevents the silent "every launch immediately blocks" failure mode.
|
|
func (h *Handler) applyRoutingOverride(
|
|
c *gin.Context, agent *models.AgentInstance, ov routing.AgentOverrides,
|
|
) error {
|
|
known := h.svc.KnownProviders()
|
|
cfg, err := h.svc.GetWorkspaceRouting(c.Request.Context(), agent.WorkspaceID)
|
|
if err != nil {
|
|
h.logger.Warn("workspace routing lookup failed; skipping cross-check",
|
|
zap.String("workspace_id", agent.WorkspaceID), zap.Error(err))
|
|
cfg = nil
|
|
}
|
|
if vErr := routing.ValidateAgentOverridesAgainstWorkspace(ov, known, cfg); vErr != nil {
|
|
respondRoutingValidation(c, vErr)
|
|
return vErr
|
|
}
|
|
merged, err := routing.WriteAgentOverrides(agent.Settings, ov)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return err
|
|
}
|
|
agent.Settings = merged
|
|
return nil
|
|
}
|
|
|
|
// respondRoutingValidation translates a ValidationError into the same
|
|
// structured 400 shape the dashboard routing handler uses.
|
|
func respondRoutingValidation(c *gin.Context, err error) {
|
|
var ve *routing.ValidationError
|
|
if errors.As(err, &ve) {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": ve.Message,
|
|
"field": ve.Field,
|
|
"details": ve.Details,
|
|
})
|
|
return
|
|
}
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
}
|
|
|
|
func (h *Handler) deleteAgent(c *gin.Context) {
|
|
if caller := agentCallerFromCtx(c); caller != nil {
|
|
if !isAdminRole(caller.Role) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "only CEO or admin can delete agents"})
|
|
return
|
|
}
|
|
}
|
|
if err := h.svc.DeleteAgentInstance(c.Request.Context(), c.Param("id")); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) updateAgentStatus(c *gin.Context) {
|
|
if caller := agentCallerFromCtx(c); caller != nil {
|
|
targetID := c.Param("id")
|
|
if targetID != caller.ID && !isAdminRole(caller.Role) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "only CEO or admin can change other agents' status"})
|
|
return
|
|
}
|
|
}
|
|
var req UpdateAgentStatusRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
agent, err := h.svc.UpdateAgentStatus(
|
|
c.Request.Context(), c.Param("id"),
|
|
models.AgentStatus(req.Status), req.PauseReason)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, AgentResponse{Agent: agent})
|
|
}
|
|
|
|
// -- Instruction handlers --
|
|
|
|
// isValidFilename checks that a filename does not contain path separators or traversals.
|
|
func isValidFilename(s string) bool {
|
|
return s != "" && !strings.Contains(s, "/") && !strings.Contains(s, "\\") && !strings.Contains(s, "..")
|
|
}
|
|
|
|
func (h *Handler) listInstructions(c *gin.Context) {
|
|
files, err := h.svc.ListInstructions(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, InstructionListResponse{Files: files})
|
|
}
|
|
|
|
func (h *Handler) getInstruction(c *gin.Context) {
|
|
filename := c.Param("filename")
|
|
if !isValidFilename(filename) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"})
|
|
return
|
|
}
|
|
file, err := h.svc.GetInstruction(c.Request.Context(), c.Param("id"), filename)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, InstructionFileResponse{File: file})
|
|
}
|
|
|
|
func (h *Handler) upsertInstruction(c *gin.Context) {
|
|
if caller := agentCallerFromCtx(c); caller != nil {
|
|
if caller.ID != c.Param("id") && !isAdminRole(caller.Role) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "only CEO or admin can modify another agent's instructions"})
|
|
return
|
|
}
|
|
}
|
|
var req UpsertInstructionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
agentID := c.Param("id")
|
|
filename := c.Param("filename")
|
|
if !isValidFilename(filename) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"})
|
|
return
|
|
}
|
|
isEntry := filename == "AGENTS.md"
|
|
if err := h.svc.UpsertInstruction(c.Request.Context(), agentID, filename, req.Content, isEntry); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
file, err := h.svc.GetInstruction(c.Request.Context(), agentID, filename)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, InstructionFileResponse{File: file})
|
|
}
|
|
|
|
func (h *Handler) deleteInstruction(c *gin.Context) {
|
|
if caller := agentCallerFromCtx(c); caller != nil {
|
|
if caller.ID != c.Param("id") && !isAdminRole(caller.Role) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "only CEO or admin can modify another agent's instructions"})
|
|
return
|
|
}
|
|
}
|
|
filename := c.Param("filename")
|
|
if !isValidFilename(filename) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid filename"})
|
|
return
|
|
}
|
|
if err := h.svc.DeleteInstruction(c.Request.Context(), c.Param("id"), filename); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// -- Utilization handler --
|
|
|
|
func (h *Handler) getAgentUtilization(c *gin.Context) {
|
|
usage, err := h.svc.GetAgentUtilization(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if usage == nil {
|
|
c.JSON(http.StatusOK, gin.H{"utilization": nil})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"utilization": usage})
|
|
}
|
|
|
|
// -- Memory handlers --
|
|
|
|
// checkAgentMemoryAccess returns an error and writes 403 when an agent
|
|
// caller attempts to access another agent's memory. UI callers (no agent
|
|
// JWT) pass through unchanged. The CEO/admin role is allowed cross-agent
|
|
// memory access to mirror the existing instruction-handler convention.
|
|
func checkAgentMemoryAccess(c *gin.Context) error {
|
|
caller := agentCallerFromCtx(c)
|
|
if caller == nil {
|
|
return nil
|
|
}
|
|
if caller.ID == c.Param("id") || isAdminRole(caller.Role) {
|
|
return nil
|
|
}
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "agents may only access their own memory"})
|
|
return shared.ErrForbidden
|
|
}
|
|
|
|
func (h *Handler) listMemory(c *gin.Context) {
|
|
if err := checkAgentMemoryAccess(c); err != nil {
|
|
return
|
|
}
|
|
layer := c.Query("layer")
|
|
entries, err := h.svc.ListMemory(c.Request.Context(), c.Param("id"), layer)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, MemoryListResponse{Memory: entries})
|
|
}
|
|
|
|
func (h *Handler) upsertMemory(c *gin.Context) {
|
|
if err := checkAgentMemoryAccess(c); err != nil {
|
|
return
|
|
}
|
|
var req UpsertMemoryRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
agentID := c.Param("id")
|
|
for _, entry := range req.Entries {
|
|
mem := &models.AgentMemory{
|
|
AgentProfileID: agentID,
|
|
Layer: entry.Layer,
|
|
Key: entry.Key,
|
|
Content: entry.Content,
|
|
Metadata: entry.Metadata,
|
|
}
|
|
if err := h.svc.UpsertAgentMemory(c.Request.Context(), mem); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) deleteAllMemory(c *gin.Context) {
|
|
if err := checkAgentMemoryAccess(c); err != nil {
|
|
return
|
|
}
|
|
if err := h.svc.DeleteAllMemory(c.Request.Context(), c.Param("id")); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) deleteMemory(c *gin.Context) {
|
|
agentID := c.Param("id")
|
|
entryID := c.Param("entryId")
|
|
if err := h.svc.DeleteAgentMemoryOwned(c.Request.Context(), agentID, entryID); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "memory entry not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) memorySummary(c *gin.Context) {
|
|
if err := checkAgentMemoryAccess(c); err != nil {
|
|
return
|
|
}
|
|
entries, err := h.svc.GetMemorySummary(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"count": len(entries), "memory": entries})
|
|
}
|
|
|
|
func (h *Handler) exportMemory(c *gin.Context) {
|
|
if err := checkAgentMemoryAccess(c); err != nil {
|
|
return
|
|
}
|
|
entries, err := h.svc.ExportMemory(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, MemoryListResponse{Memory: entries})
|
|
}
|
|
|
|
// -- Permission helpers --
|
|
|
|
// checkAgentPermission returns nil and does nothing for UI requests.
|
|
// For agent callers, it checks the specified permission and returns 403 if denied.
|
|
func checkAgentPermission(c *gin.Context, permKey string) error {
|
|
caller := agentCallerFromCtx(c)
|
|
if caller == nil {
|
|
return nil
|
|
}
|
|
perms := shared.ResolvePermissions(shared.AgentRole(caller.Role), caller.Permissions)
|
|
if !shared.HasPermission(perms, permKey) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: missing " + permKey})
|
|
return shared.ErrForbidden
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkNoEscalation verifies an agent caller is not granting permissions it
|
|
// does not itself hold. No-op for UI requests or empty permission strings.
|
|
func checkNoEscalation(c *gin.Context, permsJSON string) error {
|
|
caller := agentCallerFromCtx(c)
|
|
if caller == nil || permsJSON == "" || permsJSON == "{}" {
|
|
return nil
|
|
}
|
|
callerPerms := shared.ResolvePermissions(shared.AgentRole(caller.Role), caller.Permissions)
|
|
if err := shared.ValidateNoEscalation(callerPerms, permsJSON); err != nil {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isAdminRole returns true for roles that have administrative privileges.
|
|
func isAdminRole(role models.AgentRole) bool {
|
|
return role == models.AgentRoleCEO
|
|
}
|
|
|
|
func applyAgentUpdates(agent *models.AgentInstance, req *UpdateAgentRequest) {
|
|
if req.Name != nil {
|
|
agent.Name = *req.Name
|
|
}
|
|
// Wave G: AgentInstance.ID is the agent_profiles row id; the legacy
|
|
// AgentProfileID request field is now a no-op for updates (the row's id
|
|
// cannot change after creation). Accepting the field keeps existing
|
|
// clients valid; the value is intentionally ignored.
|
|
if req.Role != nil {
|
|
agent.Role = models.AgentRole(*req.Role)
|
|
}
|
|
if req.Icon != nil {
|
|
agent.Icon = *req.Icon
|
|
}
|
|
if req.Status != nil {
|
|
agent.Status = models.AgentStatus(*req.Status)
|
|
}
|
|
if req.ReportsTo != nil {
|
|
agent.ReportsTo = *req.ReportsTo
|
|
}
|
|
if req.Permissions != nil {
|
|
agent.Permissions = *req.Permissions
|
|
}
|
|
if req.BudgetMonthlyCents != nil {
|
|
agent.BudgetMonthlyCents = *req.BudgetMonthlyCents
|
|
}
|
|
if req.MaxConcurrentSessions != nil {
|
|
agent.MaxConcurrentSessions = *req.MaxConcurrentSessions
|
|
}
|
|
if req.DesiredSkills != nil {
|
|
agent.DesiredSkills = *req.DesiredSkills
|
|
}
|
|
if req.SkillIDs != nil {
|
|
agent.SkillIDs = *req.SkillIDs
|
|
}
|
|
if req.ExecutorPreference != nil {
|
|
agent.ExecutorPreference = *req.ExecutorPreference
|
|
}
|
|
if req.PauseReason != nil {
|
|
agent.PauseReason = *req.PauseReason
|
|
}
|
|
if req.AutoApprove != nil {
|
|
agent.AutoApprove = *req.AutoApprove
|
|
}
|
|
if req.AllowIndexing != nil {
|
|
agent.AllowIndexing = *req.AllowIndexing
|
|
}
|
|
if req.CLIPassthrough != nil {
|
|
agent.CLIPassthrough = *req.CLIPassthrough
|
|
}
|
|
}
|