1// guard — phase 3 example: event subscriptions + tool-call interception.
2//
3// What it does:
4//
5// - Subscribes to turn_start, tool_call, and turn_end events and
6// writes a one-line audit entry per event to its own log file.
7//
8// - Intercepts every tool call. If the tool is `bash` and the
9// command matches a danger pattern (rm -rf, sudo, dd of=/, etc.)
10// the call is refused and the model gets a one-line refusal it
11// can react to. Everything else passes through.
12//
13// Build it:
14//
15// cd examples/extensions/guard
16// go build -o guard .
17//
18// Drop it next to its extension.json under $ZOT_HOME/extensions/guard/
19// (or `zot ext install ./guard` from this directory) and the next zot
20// session will load it automatically.
21package main
22
23import (
24 "encoding/json"
25 "fmt"
26 "os"
27 "path/filepath"
28 "regexp"
29 "strings"
30 "time"
31
32 "github.com/patriceckhart/zot/pkg/zotext"
33)
34
35var dangerPatterns = []*regexp.Regexp{
36 regexp.MustCompile(`(?i)\brm\s+-rf\b`),
37 regexp.MustCompile(`(?i)\bsudo\b`),
38 regexp.MustCompile(`(?i)\bdd\s+of=/`),
39 regexp.MustCompile(`(?i)\bmkfs\b`),
40 regexp.MustCompile(`(?i)\b:\s*\(\s*\)\s*\{\s*:\|:`), // fork bomb
41 regexp.MustCompile(`(?i)\bchmod\s+-R\s+777\b`),
42}
43
44func main() {
45 ext := zotext.New("guard", "1.0.0")
46
47 auditPath := filepath.Join(os.TempDir(), "zot-guard-audit.log")
48 auditFile, _ := os.OpenFile(auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
49 if auditFile != nil {
50 defer auditFile.Close()
51 }
52 audit := func(format string, args ...any) {
53 line := fmt.Sprintf("%s "+format+"\n", append([]any{time.Now().Format(time.RFC3339)}, args...)...)
54 if auditFile != nil {
55 auditFile.WriteString(line)
56 }
57 ext.Logf("%s", strings.TrimRight(line, "\n"))
58 }
59
60 // Lifecycle observers.
61 ext.On("session_start", func(ev zotext.Event) { audit("[guard] session_start") })
62 ext.On("turn_start", func(ev zotext.Event) { audit("[guard] turn_start step=%d", ev.Step) })
63 ext.On("tool_call", func(ev zotext.Event) {
64 audit("[guard] tool_call %s args=%s", ev.ToolName, string(ev.ToolArgs))
65 })
66 ext.On("turn_end", func(ev zotext.Event) { audit("[guard] turn_end stop=%s err=%s", ev.Stop, ev.Error) })
67
68 // Tool-call gate.
69 ext.InterceptToolCall(func(toolName string, args json.RawMessage) (bool, string) {
70 if toolName != "bash" {
71 return true, "" // only guard bash
72 }
73 var in struct {
74 Command string `json:"command"`
75 }
76 if err := json.Unmarshal(args, &in); err != nil {
77 return true, ""
78 }
79 for _, pat := range dangerPatterns {
80 if pat.MatchString(in.Command) {
81 audit("[guard] BLOCKED bash: %s (matched %s)", in.Command, pat.String())
82 return false, fmt.Sprintf("guard refused: command matches the danger pattern %q. Ask the user before running this.", pat.String())
83 }
84 }
85 return true, ""
86 })
87
88 if err := ext.Run(); err != nil {
89 ext.Logf("fatal: %v", err)
90 }
91}