Files
noema/cmd/noemactl/capabilities.go
T
Andy Dixon b35dbe0299 security: add the capability and policy layer as the only path to action
Phase 12 (ADR-0005). Capabilities are registered in code with parameter
schemas, risk classes and default effects; sudo_shell and delete_database
are locked to DENY and cannot be relaxed. Policies are scoped globally or
per mind with capability, target glob, action kind and optional hour
window; evaluation is deterministic, most specific wins, and ties go to
the stricter effect. Invoke is the single entry point: it refuses
dreaming and replay contexts, validates parameters (unknown keys, non
http URLs and path traversal rejected), evaluates policy and session
grants, hashes the exact request and records a policy_decision event.
ALLOW executes with timeout and panic isolation and turns textual output
into a tool-trust observation; ASK creates an approval request bound to
the hash with an alert notification; DENY is counted and recorded.

Operators approve once, approve for a time-boxed session grant, deny
with a note, or modify parameters (re-validated, re-hashed, original
kept in the append-only decision log). The self-model's capability list
is recomputed from effective policy on start and on every policy change.
The act process maps fetch requests to http.fetch and the action handler
invokes through the layer; replies report capabilities and outcomes.

Policy matrix and approval centre pages, dashboard count, API, CLI
(approvals, policy, invoke) and docs/capabilities.md.
2026-06-29 20:28:05 +01:00

157 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"text/tabwriter"
)
func init() {
extraCommands = append(extraCommands,
command{"approvals", "list|approve <id> [note]|session <id>|deny <id> [note] – approval centre", cmdApprovals},
command{"policy", "list|add -mind m -capability c -target t -effect e [-reason r]|delete <id> – capability policies", cmdPolicy},
command{"invoke", "<mind> <capability> [target] [json-params] – invoke a capability through policy", cmdInvoke},
)
}
func cmdApprovals(args []string) int {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: noemactl approvals list | approve <id> [note] | session <id> [note] | deny <id> [note]")
return exitUsage
}
switch args[0] {
case "list":
var out struct{ Items []map[string]any }
if code, err := apiRequest("GET", "/api/v1/approvals?status=pending", nil, &out); err != nil {
return exitFor(code, err)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "RISK\tCAPABILITY\tTARGET\tREASON\tID")
for _, a := range out.Items {
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\n", a["risk"], a["capability"], a["target"], a["reason"], a["id"])
}
tw.Flush()
return exitOK
case "approve", "session", "deny":
if len(args) < 2 {
return exitUsage
}
note := strings.Join(args[2:], " ")
path := "/api/v1/approvals/" + args[1] + "/approve"
body := map[string]any{"note": note, "for_session": args[0] == "session"}
if args[0] == "deny" {
path = "/api/v1/approvals/" + args[1] + "/deny"
body = map[string]any{"note": note}
}
var res map[string]any
if code, err := apiRequest("POST", path, body, &res); err != nil {
return exitFor(code, err)
}
if st, ok := res["status"]; ok {
fmt.Println("result:", st)
if r, ok := res["result"].(map[string]any); ok {
fmt.Println(r["summary"])
}
} else {
fmt.Println("recorded")
}
return exitOK
}
return exitUsage
}
func cmdPolicy(args []string) int {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: noemactl policy list | add -mind m -capability c -target t -effect allow|ask|deny [-reason r] | delete <id>")
return exitUsage
}
switch args[0] {
case "list":
var out struct{ Items []map[string]any }
if code, err := apiRequest("GET", "/api/v1/policies", nil, &out); err != nil {
return exitFor(code, err)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "SCOPE\tCAPABILITY\tTARGET\tEFFECT\tREASON\tID")
for _, p := range out.Items {
scope := "global"
if m, _ := p["mind_id"].(string); m != "" {
scope = m[:8]
}
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%v\n", scope, p["capability"], p["target_pattern"], p["effect"], p["reason"], p["id"])
}
tw.Flush()
return exitOK
case "add":
body := map[string]any{"target_pattern": "*"}
for i := 1; i+1 < len(args); i += 2 {
switch args[i] {
case "-mind":
var m map[string]any
if code, err := apiRequest("GET", "/api/v1/minds/"+args[i+1], nil, &m); err != nil {
return exitFor(code, err)
}
body["mind_id"] = m["id"]
case "-capability":
body["capability"] = args[i+1]
case "-target":
body["target_pattern"] = args[i+1]
case "-effect":
body["effect"] = args[i+1]
case "-reason":
body["reason"] = args[i+1]
}
}
var p map[string]any
if code, err := apiRequest("POST", "/api/v1/policies", body, &p); err != nil {
return exitFor(code, err)
}
fmt.Println("policy", p["id"], "added")
return exitOK
case "delete":
if len(args) < 2 {
return exitUsage
}
if code, err := apiRequest("DELETE", "/api/v1/policies/"+args[1], nil, nil); err != nil {
return exitFor(code, err)
}
fmt.Println("deleted")
return exitOK
}
return exitUsage
}
func cmdInvoke(args []string) int {
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "usage: noemactl invoke <mind> <capability> [target] [json-params]")
return exitUsage
}
body := map[string]any{"capability": args[1]}
if len(args) > 2 {
body["target"] = args[2]
}
if len(args) > 3 {
var params map[string]any
if err := json.Unmarshal([]byte(args[3]), &params); err != nil {
fmt.Fprintln(os.Stderr, "params must be a JSON object")
return exitUsage
}
body["params"] = params
}
var out map[string]any
if code, err := apiRequest("POST", "/api/v1/minds/"+args[0]+"/invoke", body, &out); err != nil {
return exitFor(code, err)
}
d, _ := out["decision"].(map[string]any)
fmt.Printf("%v (%v: %v)\n", out["status"], d["effect"], d["reason"])
if out["approval_id"] != nil && out["approval_id"] != "" {
fmt.Println("approval:", out["approval_id"])
}
if r, ok := out["result"].(map[string]any); ok {
fmt.Println(r["summary"])
}
return exitOK
}