mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-11 10:10:53 +00:00
843b550651
Runtime package management with security hardening: - pkg-helper: root-privileged daemon for apk install/uninstall via Unix socket - HTTP API: /v1/packages (list/install/uninstall/runtimes), admin role required for writes - Shell deny groups: 15 configurable groups (per-agent overrides via context) - Packages UI: Web page for managing system/pip/npm packages with confirmation dialogs - Docker: privilege separation (root entrypoint → su-exec drop), init for zombie reaping - Security: umask socket creation, persist file validation, deny pattern hardening (Node.js fetch/http, Python from/import, curl localhost, sensitive env vars) - Auth: empty gateway token → admin role (dev/single-user mode)
135 lines
4.8 KiB
Go
135 lines
4.8 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"regexp"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/permissions"
|
|
"github.com/nextlevelbuilder/goclaw/internal/skills"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
)
|
|
|
|
// validPkgName allows alphanumeric, hyphens, underscores, dots, @, / (for scoped npm).
|
|
// Rejects names starting with - to prevent argument injection.
|
|
var validPkgName = regexp.MustCompile(`^[a-zA-Z0-9@][a-zA-Z0-9._+\-/@]*$`)
|
|
|
|
// PackagesHandler handles runtime package management HTTP endpoints.
|
|
type PackagesHandler struct {
|
|
token string
|
|
}
|
|
|
|
// NewPackagesHandler creates a handler for package management endpoints.
|
|
func NewPackagesHandler(token string) *PackagesHandler {
|
|
return &PackagesHandler{token: token}
|
|
}
|
|
|
|
// RegisterRoutes registers all package management routes on the given mux.
|
|
func (h *PackagesHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /v1/packages", h.readAuth(h.handleList))
|
|
mux.HandleFunc("POST /v1/packages/install", h.adminAuth(h.handleInstall))
|
|
mux.HandleFunc("POST /v1/packages/uninstall", h.adminAuth(h.handleUninstall))
|
|
mux.HandleFunc("GET /v1/packages/runtimes", h.readAuth(h.handleRuntimes))
|
|
mux.HandleFunc("GET /v1/shell-deny-groups", h.readAuth(h.handleDenyGroups))
|
|
}
|
|
|
|
// readAuth allows viewer+ for read operations.
|
|
func (h *PackagesHandler) readAuth(next http.HandlerFunc) http.HandlerFunc {
|
|
return requireAuth(h.token, "", next)
|
|
}
|
|
|
|
// adminAuth requires admin role for write operations (install/uninstall).
|
|
// Prevents agents from calling these endpoints even if they obtain the gateway token,
|
|
// since agent requests via browser pairing only get operator role.
|
|
func (h *PackagesHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc {
|
|
return requireAuth(h.token, permissions.RoleAdmin, next)
|
|
}
|
|
|
|
// handleList returns all installed packages grouped by category (system/pip/npm).
|
|
func (h *PackagesHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
|
pkgs := skills.ListInstalledPackages(r.Context())
|
|
writeJSON(w, http.StatusOK, pkgs)
|
|
}
|
|
|
|
// parseAndValidatePackage reads and validates a package name from the request body.
|
|
// Returns the validated package string or writes an error response and returns empty.
|
|
func parseAndValidatePackage(w http.ResponseWriter, r *http.Request) string {
|
|
r.Body = http.MaxBytesReader(w, r.Body, 4096)
|
|
var body struct {
|
|
Package string `json:"package"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Package == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "package required"})
|
|
return ""
|
|
}
|
|
|
|
// Strip prefix for validation, then validate the bare package name.
|
|
name := body.Package
|
|
for _, prefix := range []string{"pip:", "npm:"} {
|
|
if len(name) > len(prefix) && name[:len(prefix)] == prefix {
|
|
name = name[len(prefix):]
|
|
break
|
|
}
|
|
}
|
|
if !validPkgName.MatchString(name) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid package name"})
|
|
return ""
|
|
}
|
|
|
|
return body.Package
|
|
}
|
|
|
|
// handleInstall installs a single package.
|
|
// Body: {"package": "github-cli"} or {"package": "pip:pandas"} or {"package": "npm:typescript"}
|
|
func (h *PackagesHandler) handleInstall(w http.ResponseWriter, r *http.Request) {
|
|
pkg := parseAndValidatePackage(w, r)
|
|
if pkg == "" {
|
|
return
|
|
}
|
|
ok, errMsg := skills.InstallSingleDep(r.Context(), pkg)
|
|
if !ok {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"ok": false, "error": errMsg})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleUninstall removes a single package.
|
|
// Body: {"package": "github-cli"} or {"package": "pip:pandas"} or {"package": "npm:typescript"}
|
|
func (h *PackagesHandler) handleUninstall(w http.ResponseWriter, r *http.Request) {
|
|
pkg := parseAndValidatePackage(w, r)
|
|
if pkg == "" {
|
|
return
|
|
}
|
|
ok, errMsg := skills.UninstallPackage(r.Context(), pkg)
|
|
if !ok {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"ok": false, "error": errMsg})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// handleRuntimes returns the availability of prerequisite runtimes.
|
|
func (h *PackagesHandler) handleRuntimes(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, skills.CheckRuntimes())
|
|
}
|
|
|
|
// handleDenyGroups returns all registered shell deny groups with name, description, and default state.
|
|
func (h *PackagesHandler) handleDenyGroups(w http.ResponseWriter, _ *http.Request) {
|
|
type groupInfo struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Default bool `json:"default"`
|
|
}
|
|
groups := make([]groupInfo, 0, len(tools.DenyGroupRegistry))
|
|
for _, name := range tools.DenyGroupNames() {
|
|
g := tools.DenyGroupRegistry[name]
|
|
groups = append(groups, groupInfo{
|
|
Name: g.Name,
|
|
Description: g.Description,
|
|
Default: g.Default,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"groups": groups})
|
|
}
|