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.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package cognition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/andydixon/noema/internal/attention"
|
||||
"github.com/andydixon/noema/internal/brain"
|
||||
"github.com/andydixon/noema/internal/capabilities"
|
||||
"github.com/andydixon/noema/internal/conversation"
|
||||
"github.com/andydixon/noema/internal/workspace"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/pkg/ids"
|
||||
)
|
||||
|
||||
// Act proposes capability invocations from requests the mind receives. In
|
||||
// this phase the only deterministic mapping is "fetch <url>" → http.fetch;
|
||||
// integrations register more. Proposing is free; executing goes through the
|
||||
// policy layer, so a request never bypasses ALLOW / ASK / DENY.
|
||||
type Act struct{}
|
||||
|
||||
// Name implements brain.Process.
|
||||
func (Act) Name() string { return "act" }
|
||||
|
||||
var urlRe = regexp.MustCompile(`https?://[^\s<>"']+`)
|
||||
|
||||
// Observe implements brain.Process.
|
||||
func (Act) Observe(_ context.Context, s *brain.State) ([]brain.Candidate, error) {
|
||||
if s.Services.Capabilities == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var out []brain.Candidate
|
||||
for _, ev := range s.NewEvents {
|
||||
if ev.Type != events.Observation || ev.Metadata["conversation_id"] == nil {
|
||||
continue
|
||||
}
|
||||
var p events.ObservationPayload
|
||||
if json.Unmarshal(ev.Payload, &p) != nil {
|
||||
continue
|
||||
}
|
||||
a := conversation.Analyse(p.Text)
|
||||
if a.Intent != "request" {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(p.Text)
|
||||
if u := urlRe.FindString(p.Text); u != "" && (strings.Contains(lower, "fetch") || strings.Contains(lower, "read") || strings.Contains(lower, "look at") || strings.Contains(lower, "check")) {
|
||||
u = strings.TrimRight(u, ".,)")
|
||||
out = append(out, brain.Candidate{Kind: "action", Subject: "http.fetch:" + u, Summary: "proposed action: fetch " + u,
|
||||
Content: map[string]any{"capability": "http.fetch", "target": u, "params": map[string]any{"url": u}, "reason": "asked by " + p.Actor + ": " + truncate(p.Text, 120), "source_event": string(ev.ID), "conversation_id": ev.Metadata["conversation_id"]},
|
||||
Features: attention.Features{Salience: 0.7, Urgency: 0.6, Consequence: 0.7, Confidence: 0.9, OperatorPriority: prioFor(p.Trust), ContextRelevance: 0.5},
|
||||
Confidence: 0.9, SourceEvents: []ids.ID{ev.ID}, Evidence: []events.EvidenceRef{{Kind: "event", ID: string(ev.ID), Note: "request"}}})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func prioFor(t events.Trust) float64 {
|
||||
if t == events.TrustOperator {
|
||||
return 1
|
||||
}
|
||||
return 0.7
|
||||
}
|
||||
|
||||
// action invokes the capability layer for an admitted action thought and
|
||||
// records the outcome in working memory. The layer decides; this handler
|
||||
// never executes anything itself.
|
||||
func (h Handlers) action(ctx context.Context, c *brain.Cycle, t workspace.Thought) error {
|
||||
svc := c.State.Services.Capabilities
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
capName, _ := t.Content["capability"].(string)
|
||||
target, _ := t.Content["target"].(string)
|
||||
reason, _ := t.Content["reason"].(string)
|
||||
params, _ := t.Content["params"].(map[string]any)
|
||||
var ev []capabilities.EvidenceRef
|
||||
for _, e := range t.Evidence {
|
||||
ev = append(ev, capabilities.EvidenceRef{Kind: e.Kind, ID: e.ID, Note: e.Note})
|
||||
}
|
||||
if c.State.Mode == brain.ModeDreaming {
|
||||
ctx = capabilities.WithForbidden(ctx, "dreaming")
|
||||
}
|
||||
out, err := svc.Invoke(ctx, capabilities.Invocation{MindID: c.State.Mind.ID, Capability: capName, Target: target, Params: params, Reason: reason, Evidence: ev, Correlation: c.State.CycleID})
|
||||
if err != nil {
|
||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.act", Summary: "action refused before policy: " + err.Error(), Salience: 0.4, Confidence: 1, CausationID: t.EventID, Metadata: map[string]any{"check": "action_invalid", "capability": capName}})
|
||||
return nil
|
||||
}
|
||||
content := fmt.Sprintf("%s on %s: %s", capName, target, out.Status)
|
||||
if out.Status == "pending" {
|
||||
content += " (awaiting operator approval)"
|
||||
}
|
||||
if out.Status == "denied" {
|
||||
content += " (" + out.Decision.Reason + ")"
|
||||
}
|
||||
c.Runtime.Hold(workspace.Item{Key: "action:" + capName + ":" + target, Content: content, Kind: "note", SourceEvent: out.EventID, Ref: string(out.ApprovalID)})
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user