chore: sync from fn-registry agent
This commit is contained in:
+326
@@ -0,0 +1,326 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CapabilityRequest envelope minimo POC (sin firma todavia — issue 0134 v0.2).
|
||||
type CapabilityRequest struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Capability string `json:"capability"`
|
||||
Args json.RawMessage `json:"args"`
|
||||
Nonce string `json:"nonce"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
}
|
||||
|
||||
// parseArgsArray intenta decode args como []string. Si args es un objeto
|
||||
// con campo "argv" (formato MCP), extrae argv. Si es array plano, lo devuelve.
|
||||
func parseArgsArray(raw json.RawMessage) ([]string, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Intento 1: array
|
||||
var arr []string
|
||||
if err := json.Unmarshal(raw, &arr); err == nil {
|
||||
return arr, nil
|
||||
}
|
||||
// Intento 2: object con argv
|
||||
var obj map[string]any
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return nil, fmt.Errorf("args must be array or object with argv: %w", err)
|
||||
}
|
||||
if v, ok := obj["argv"]; ok {
|
||||
switch t := v.(type) {
|
||||
case []any:
|
||||
out := make([]string, len(t))
|
||||
for i, x := range t {
|
||||
out[i] = fmt.Sprintf("%v", x)
|
||||
}
|
||||
return out, nil
|
||||
case []string:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("args object missing argv array")
|
||||
}
|
||||
|
||||
// parseArgsMap decodifica args como map[string]any para capabilities que
|
||||
// reciben objeto. Acepta tambien array (lo wrap como {argv:[...]}).
|
||||
func parseArgsMap(raw json.RawMessage) (map[string]any, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var obj map[string]any
|
||||
if err := json.Unmarshal(raw, &obj); err == nil {
|
||||
return obj, nil
|
||||
}
|
||||
// Fallback: array → {argv: [...]}
|
||||
var arr []any
|
||||
if err := json.Unmarshal(raw, &arr); err != nil {
|
||||
return nil, fmt.Errorf("args must be object or array: %w", err)
|
||||
}
|
||||
return map[string]any{"argv": arr}, nil
|
||||
}
|
||||
|
||||
type CapabilityResponse struct {
|
||||
RequestID string `json:"request_id"`
|
||||
OK bool `json:"ok"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
AuditHash string `json:"audit_hash,omitempty"`
|
||||
}
|
||||
|
||||
func capabilityHandler(mf *Manifest, audit *Audit) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "POST only")
|
||||
return
|
||||
}
|
||||
var req CapabilityRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
||||
return
|
||||
}
|
||||
if req.RequestID == "" || req.Capability == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "request_id and capability required")
|
||||
return
|
||||
}
|
||||
|
||||
cap := mf.CapabilityByName(req.Capability)
|
||||
if cap == nil {
|
||||
writeError(w, http.StatusForbidden, "capability_denied",
|
||||
fmt.Sprintf("capability %q not in manifest", req.Capability))
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
var result any
|
||||
var execErr error
|
||||
exitCode := 0
|
||||
var shellEvalExtra *ShellEvalRecord
|
||||
|
||||
switch req.Capability {
|
||||
case "shell.exec":
|
||||
argv, perr := parseArgsArray(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runShellExec(cap, argv)
|
||||
case "shell.eval":
|
||||
rawMap, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
var r map[string]any
|
||||
r, exitCode, shellEvalExtra, execErr = runShellEval(cap, rawMap)
|
||||
result = r
|
||||
case "fs.read":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runFsRead(cap, m)
|
||||
case "fs.write":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runFsWrite(cap, m)
|
||||
case "fs.list":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runFsList(cap, m)
|
||||
case "fs.stat":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runFsStat(cap, m)
|
||||
case "git.clone":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runGitClone(cap, m)
|
||||
case "git.commit":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runGitCommit(cap, m)
|
||||
case "git.push":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runGitPush(cap, m)
|
||||
case "pkg.search":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runPkgSearch(cap, m)
|
||||
case "proc.list":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runProcList(cap, m)
|
||||
case "docker.container.list":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runDockerList(cap, m)
|
||||
case "docker.container.exec":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runDockerExec(cap, m)
|
||||
case "docker.container.logs":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
result, exitCode, execErr = runDockerLogs(cap, m)
|
||||
case "browser.list_tabs", "browser.navigate", "browser.click_text",
|
||||
"browser.evaluate", "browser.screenshot", "browser.type_text",
|
||||
"browser.get_html", "browser.launch_chrome":
|
||||
m, perr := parseArgsMap(req.Args)
|
||||
if perr != nil {
|
||||
execErr = perr
|
||||
break
|
||||
}
|
||||
op := strings.TrimPrefix(req.Capability, "browser.")
|
||||
result, exitCode, execErr = runBrowserOp(cap, op, m)
|
||||
default:
|
||||
execErr = fmt.Errorf("capability %q not implemented yet in POC", req.Capability)
|
||||
}
|
||||
|
||||
dur := time.Since(start).Milliseconds()
|
||||
|
||||
var hash string
|
||||
var herr error
|
||||
if req.Capability == "shell.eval" && shellEvalExtra != nil {
|
||||
hash, herr = audit.AppendVerbose(req.RequestID, req.Capability, req.Args, exitCode, *shellEvalExtra)
|
||||
} else {
|
||||
hash, herr = audit.Append(req.RequestID, req.Capability, req.Args, exitCode)
|
||||
}
|
||||
if herr != nil {
|
||||
// audit es critico — si falla, devolvemos error pero no perdemos el output.
|
||||
writeJSON(w, http.StatusInternalServerError, CapabilityResponse{
|
||||
RequestID: req.RequestID,
|
||||
OK: false,
|
||||
Error: "audit_failed: " + herr.Error(),
|
||||
DurationMs: dur,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resp := CapabilityResponse{
|
||||
RequestID: req.RequestID,
|
||||
DurationMs: dur,
|
||||
AuditHash: hash,
|
||||
}
|
||||
if execErr != nil {
|
||||
resp.OK = false
|
||||
resp.Error = execErr.Error()
|
||||
writeJSON(w, http.StatusInternalServerError, resp)
|
||||
return
|
||||
}
|
||||
resp.OK = true
|
||||
resp.Result = result
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// parseShellEvalArgs decodifica los args de la envelope shell.eval.
|
||||
// Convencion: req.Args puede ser:
|
||||
// - len(args) == 1 y args[0] es un JSON object {"cmd":..., "shell":..., "cwd":...}
|
||||
// - len(args) >= 1 y args[0] = cmd, args[1] (opcional) = cwd, args[2] (opcional) = shell
|
||||
//
|
||||
// Devuelve un map[string]any compatible con runShellEval.
|
||||
func parseShellEvalArgs(args []string) (map[string]any, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("shell.eval requires at least 1 arg")
|
||||
}
|
||||
// Intento 1: JSON object string.
|
||||
trim := strings.TrimSpace(args[0])
|
||||
if strings.HasPrefix(trim, "{") && strings.HasSuffix(trim, "}") {
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(trim), &m); err == nil {
|
||||
return m, nil
|
||||
}
|
||||
// caemos al fallback
|
||||
}
|
||||
out := map[string]any{
|
||||
"cmd": args[0],
|
||||
}
|
||||
if len(args) >= 2 && args[1] != "" {
|
||||
out["cwd"] = args[1]
|
||||
}
|
||||
if len(args) >= 3 && args[2] != "" {
|
||||
out["shell"] = args[2]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// runShellExec implementa shell.exec con whitelist del manifest local.
|
||||
// POC INLINE: el flow 0009 v0.2 lo migra a shell_exec_whitelist_go_infra del registry.
|
||||
func runShellExec(cap *Capability, args []string) (any, int, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, -1, fmt.Errorf("no argv provided")
|
||||
}
|
||||
if len(cap.BinariesAllowed) == 0 {
|
||||
return nil, -1, fmt.Errorf("no binaries whitelisted for shell.exec")
|
||||
}
|
||||
bin := args[0]
|
||||
allowed := false
|
||||
for _, b := range cap.BinariesAllowed {
|
||||
if b == bin || strings.HasSuffix(bin, "/"+b) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return nil, -1, fmt.Errorf("binary %q not in whitelist %v", bin, cap.BinariesAllowed)
|
||||
}
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...) // #nosec G204 — whitelist enforced above
|
||||
out, err := cmd.CombinedOutput()
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
exitCode = ee.ExitCode()
|
||||
} else {
|
||||
return nil, -1, err
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"stdout": string(out),
|
||||
"exit_code": exitCode,
|
||||
}, exitCode, nil
|
||||
}
|
||||
Reference in New Issue
Block a user