diff --git a/clavis/clavis-vault/api/handlers.go b/clavis/clavis-vault/api/handlers.go index 62befd7..407bcda 100644 --- a/clavis/clavis-vault/api/handlers.go +++ b/clavis/clavis-vault/api/handlers.go @@ -1094,7 +1094,8 @@ func (h *Handlers) HandleCreateAgent(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) HandleListAgents(w http.ResponseWriter, r *http.Request) { - if h.requireAdmin(w, r) { + // Only web users (not agents) can list agents + if h.requireOwner(w, r) { return } entries, err := lib.EntryList(h.db(r), h.vk(r), nil) @@ -1148,6 +1149,53 @@ func (h *Handlers) HandleDeleteAgent(w http.ResponseWriter, r *http.Request) { JSONResponse(w, http.StatusOK, map[string]string{"status": "deleted"}) } +func (h *Handlers) HandleUpdateAgent(w http.ResponseWriter, r *http.Request) { + if h.requireAdmin(w, r) { + return + } + entryID, err := lib.HexToID(chi.URLParam(r, "id")) + if err != nil { + ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID") + return + } + entry, _ := lib.EntryGet(h.db(r), h.vk(r), entryID) + if entry == nil || entry.Type != lib.TypeAgent { + ErrorResponse(w, http.StatusNotFound, "not_found", "Agent not found") + return + } + var req struct { + IPWhitelist []string `json:"ip_whitelist"` + RateLimitMinute int `json:"rate_limit_minute"` + RateLimitHour int `json:"rate_limit_hour"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") + return + } + // Update allowed fields only (NOT admin, all_access, or scopes) + if entry.VaultData == nil { + ErrorResponse(w, http.StatusInternalServerError, "agent_data_missing", "Agent data not found") + return + } + if req.IPWhitelist != nil { + entry.VaultData.AllowedIPs = strings.Join(req.IPWhitelist, ",") + } + if req.RateLimitMinute > 0 { + entry.VaultData.RateLimit = req.RateLimitMinute + } + // Note: RateLimitHour is not stored separately in current model + // Re-encrypt and update entry + if err := lib.EntryUpdate(h.db(r), h.vk(r), entry); err != nil { + ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update agent") + return + } + lib.AuditLog(h.db(r), &lib.AuditEvent{ + Action: lib.ActionAgentUpdate, Actor: ActorFromContext(r.Context()), + IPAddr: realIP(r), Title: entry.Title, + }) + JSONResponse(w, http.StatusOK, map[string]string{"status": "updated"}) +} + func (h *Handlers) HandleUpdateEntryScopes(w http.ResponseWriter, r *http.Request) { if h.requireAdmin(w, r) { return diff --git a/clavis/clavis-vault/api/routes.go b/clavis/clavis-vault/api/routes.go index e07c094..69ddfeb 100644 --- a/clavis/clavis-vault/api/routes.go +++ b/clavis/clavis-vault/api/routes.go @@ -134,6 +134,7 @@ func mountAPIRoutes(r chi.Router, h *Handlers) { // Agent management (admin-only — requires PRF tap + admin token) r.Post("/agents", h.HandleCreateAgent) r.Get("/agents", h.HandleListAgents) + r.Put("/agents/{id}", h.HandleUpdateAgent) r.Delete("/agents/{id}", h.HandleDeleteAgent) // WebAuthn credential management