Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions cmd/governance_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package cmd

import (
"fmt"
"os"
"sort"
"strings"

"github.com/GrayCodeAI/hawk/internal/governance"
"github.com/spf13/cobra"
)

var governancePath string

// governanceCmd exposes the POLICY ∩ PROFILE permission ceiling.
var governanceCmd = &cobra.Command{
Use: "governance",
Short: "Inspect and validate the governance policy ceiling",
Long: `Governance is the administrator-set POLICY ceiling layered under the
per-session PROFILE (tightest-wins). Tools are permitted only when both
layers allow them.

hawk governance Show the managed policy status
hawk governance show Print the effective capability rows
hawk governance validate <file> Validate a policy or profile document
hawk governance explain <tool> Evaluate a tool against the policy`,
RunE: func(cmd *cobra.Command, args []string) error {
return runGovernanceStatus(cmd)
},
}

var governanceShowCmd = &cobra.Command{
Use: "show",
Short: "Print the effective policy capability rows",
RunE: func(cmd *cobra.Command, args []string) error {
layer, err := governanceLayerForCLI()
if err != nil {
return err
}
path := governancePath
if path == "" {
path = governance.ManagedPolicyPath()
}
cmd.Printf("Governance layer %q (%s)\n", layer.Name, path)
cmd.Printf("Fail-closed: %t\n", layer.FailClosed)
if len(layer.DeniedTools) > 0 {
cmd.Printf("Denied tools: %s\n", sortedKeys(layer.DeniedTools))
}
if len(layer.DeniedBash) > 0 {
cmd.Printf("Denied bash patterns: %s\n", strings.Join(layer.DeniedBash, ", "))
}
if len(layer.SensitivePaths) > 0 {
cmd.Printf("Sensitive paths: %s\n", strings.Join(layer.SensitivePaths, ", "))
}
if len(layer.Capabilities) == 0 {
cmd.Println("No capability rows.")
return nil
}
cmd.Println("\nCapabilities:")
for _, cap := range layer.Capabilities {
pattern := cap.Pattern
if pattern == "" {
pattern = "*"
}
reason := ""
if cap.Reason != "" {
reason = " (" + cap.Reason + ")"
}
cmd.Printf(" %-8s %-20s %-12s %s\n", cap.Action, cap.Scope, pattern, reason)
}
return nil
},
}

var governanceValidateCmd = &cobra.Command{
Use: "validate <file>",
Short: "Validate a governance policy or profile document",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
layer, err := governance.LoadLayer("policy", args[0])
if err != nil {
return err
}
cmd.Printf("valid: %d capability row(s), fail_closed=%t (%s)\n",
len(layer.Capabilities), layer.FailClosed, args[0])
return nil
},
}

var governanceExplainCmd = &cobra.Command{
Use: "explain <tool> [summary]",
Short: "Evaluate a tool call against the policy and show the decision",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
toolName := args[0]
summary := strings.Join(args[1:], " ")

layer, err := governanceLayerForCLI()
if err != nil {
return err
}
eng := governance.New()
eng.SetPolicy(layer)

dec := eng.Evaluate(toolName, summary)
scopes := governance.ScopesForTool(toolName)
scoped := "(ungoverned scope)"
if len(scopes) > 0 {
scoped = strings.Join(scopeNames(scopes), ", ")
}
verdict := "DENY"
if dec.Allowed {
verdict = "ALLOW"
}
cmd.Printf("tool: %s\n", toolName)
cmd.Printf("scopes: %s\n", scoped)
if summary != "" {
cmd.Printf("summary: %s\n", summary)
}
cmd.Printf("decision: %s\n", verdict)
cmd.Printf("source: %s\n", dec.Source)
if dec.Scope != "" {
cmd.Printf("scope hit: %s\n", dec.Scope)
}
if dec.Rule != "" {
cmd.Printf("rule: %s\n", dec.Rule)
}
if dec.Reason != "" {
cmd.Printf("reason: %s\n", dec.Reason)
}
return nil
},
}

func init() {
governanceShowCmd.Flags().StringVar(&governancePath, "path", "", "policy file to inspect (default: managed policy path)")
governanceExplainCmd.Flags().StringVar(&governancePath, "path", "", "policy file to evaluate against (default: managed policy path)")
governanceCmd.AddCommand(governanceShowCmd)
governanceCmd.AddCommand(governanceValidateCmd)
governanceCmd.AddCommand(governanceExplainCmd)
rootCmd.AddCommand(governanceCmd)
}

func runGovernanceStatus(cmd *cobra.Command) error {
path := governance.ManagedPolicyPath()
cmd.Printf("Managed policy path: %s\n", path)
if _, err := os.Stat(path); err != nil {
cmd.Println("Status: not installed (governance is fail-open; no ceiling enforced)")
return nil
}
layer, err := governance.LoadLayer("policy", path)
if err != nil {
return fmt.Errorf("managed policy is invalid: %w", err)
}
cmd.Printf("Status: installed — fail_closed=%t, %d capability row(s), %d denied tool(s)\n",
layer.FailClosed, len(layer.Capabilities), len(layer.DeniedTools))
return nil
}

func governanceLayerForCLI() (*governance.Layer, error) {
path := governancePath
if path == "" {
path = governance.ManagedPolicyPath()
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("no governance policy at %s; use --path to point at a policy file", path)
}
return nil, err
}
return governance.LoadLayer("policy", path)
}

func sortedKeys(m map[string]struct{}) string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return strings.Join(keys, ", ")
}

func scopeNames(scopes []governance.ScopeName) []string {
names := make([]string, len(scopes))
for i, s := range scopes {
names[i] = string(s)
}
return names
}
126 changes: 126 additions & 0 deletions cmd/learn_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package cmd

import (
"fmt"
"strings"
"time"

"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/spf13/cobra"
)

var (
learnWhat string
learnWhy string
learnLesson string
learnCategory string
learnLimit int
learnAll bool
)

// learnCmd manages the cross-session lesson store.
var learnCmd = &cobra.Command{
Use: "learn",
Short: "Manage lessons learned across sessions",
Long: `Hawk persists lessons from failures (and manual entries) so future
sessions avoid repeating them. Lessons are injected into the system prompt.

hawk learn List recent lessons
hawk learn add Add a lesson manually
hawk learn prompt <context> Print the lesson-extraction prompt for a context
hawk learn clear Remove all lessons`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLearnList(cmd)
},
}

var learnAddCmd = &cobra.Command{
Use: "add",
Short: "Add a lesson manually",
RunE: func(cmd *cobra.Command, args []string) error {
if strings.TrimSpace(learnWhat) == "" || strings.TrimSpace(learnLesson) == "" {
return fmt.Errorf("--what and --lesson are required")
}
if learnCategory == "" {
learnCategory = "manual"
}
si := engine.NewSelfImprover()
si.Learn(strings.TrimSpace(learnWhat), strings.TrimSpace(learnWhy), strings.TrimSpace(learnLesson), strings.TrimSpace(learnCategory))
cmd.Printf("lesson added (category: %s)\n", learnCategory)
return nil
},
}

var learnPromptCmd = &cobra.Command{
Use: "prompt <context>",
Short: "Print the lesson-extraction prompt for a failure context",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println(engine.LearnPrompt(strings.Join(args, " ")))
return nil
},
}

var learnClearCmd = &cobra.Command{
Use: "clear",
Short: "Remove all lessons",
RunE: func(cmd *cobra.Command, args []string) error {
si := engine.NewSelfImprover()
n := len(si.Lessons(""))
if n == 0 {
cmd.Println("no lessons to clear")
return nil
}
si.Clear()
cmd.Printf("cleared %d lesson(s)\n", n)
return nil
},
}

func init() {
learnAddCmd.Flags().StringVar(&learnWhat, "what", "", "what went wrong")
learnAddCmd.Flags().StringVar(&learnWhy, "why", "", "root cause")
learnAddCmd.Flags().StringVar(&learnLesson, "lesson", "", "what to do differently")
learnAddCmd.Flags().StringVar(&learnCategory, "category", "manual", "code, test, design, communication, manual")
learnCmd.Flags().IntVar(&learnLimit, "limit", 20, "max lessons to print (0 = all)")
learnCmd.Flags().BoolVar(&learnAll, "all", false, "include all fields (also shows the why)")
learnCmd.AddCommand(learnAddCmd)
learnCmd.AddCommand(learnPromptCmd)
learnCmd.AddCommand(learnClearCmd)
rootCmd.AddCommand(learnCmd)
}

func runLearnList(cmd *cobra.Command) error {
si := engine.NewSelfImprover()
lessons := si.Lessons("")
if len(lessons) == 0 {
cmd.Println("No lessons yet. Add one with: hawk learn add --what ... --lesson ...")
return nil
}

// Count by category.
cats := map[string]int{}
for _, e := range lessons {
cats[e.Category]++
}
var catSummary []string
for cat, count := range cats {
catSummary = append(catSummary, fmt.Sprintf("%s (%d)", cat, count))
}
cmd.Printf("Lesson store: %d lesson(s) — %s\n", len(lessons), strings.Join(catSummary, ", "))

start := 0
if learnLimit > 0 && len(lessons) > learnLimit {
start = len(lessons) - learnLimit
}
cmd.Println()
for _, e := range lessons[start:] {
cmd.Printf("[%s] %s\n", e.Category, e.What)
cmd.Printf(" lesson: %s\n", e.Lesson)
if learnAll && e.Why != "" {
cmd.Printf(" why: %s\n", e.Why)
}
cmd.Printf(" learned: %s\n", e.Timestamp.Format(time.RFC3339))
}
return nil
}
Loading