1// secret — demonstrates collecting a secret from the user via a
2// masked panel. The model never sees the value; it only sees the
3// outcome (success or failure).
4//
5// Registers one LLM-callable tool:
6//
7// fetch_with_password(url: string)
8//
9// When the model calls it, a panel opens with a masked password
10// field. The user types the password and presses Enter; the tool
11// goroutine uses it directly to perform a (fake) authenticated fetch
12// and returns only "fetched successfully" or an error to the model.
13// The password exists only in the extension process's memory and is
14// never written to any JSON frame or the transcript.
15//
16// Build:
17//
18// cd examples/extensions/secret
19// go build -o secret .
20//
21// Install:
22//
23// zot ext install .
24//
25// Try it — ask zot something like:
26//
27// "Fetch https://internal.example.com/report — it needs a password."
28//
29// The model calls fetch_with_password; a masked password panel opens.
30// Type anything and press Enter. The model receives the result without
31// ever seeing what you typed.
32package main
33
34import (
35 "encoding/json"
36 "fmt"
37 "os"
38 "strings"
39 "sync"
40
41 "github.com/patriceckhart/zot/packages/agent/ext"
42)
43
44const schema = `{
45 "type": "object",
46 "properties": {
47 "url": {
48 "type": "string",
49 "description": "The URL to fetch."
50 }
51 },
52 "required": ["url"]
53}`
54
55func main() {
56 e := ext.New("secret", "1.0.0")
57
58 var mu sync.Mutex
59 var counter int
60 nextPID := func() string {
61 mu.Lock()
62 defer mu.Unlock()
63 counter++
64 return fmt.Sprintf("secret-%d", counter)
65 }
66
67 e.Tool("fetch_with_password",
68 "Fetch a URL that requires a password. The password is collected directly from the user and never exposed to the model.",
69 json.RawMessage(schema),
70 func(args json.RawMessage) ext.ToolResult {
71 var in struct {
72 URL string `json:"url"`
73 }
74 if err := json.Unmarshal(args, &in); err != nil {
75 return ext.TextErrorResult("invalid args: " + err.Error())
76 }
77 if strings.TrimSpace(in.URL) == "" {
78 return ext.TextErrorResult("url is required")
79 }
80
81 pid := nextPID()
82
83 type result struct {
84 password string
85 ok bool
86 }
87 ch := make(chan result, 1)
88
89 // input and inputMu are owned by the key handler goroutine.
90 var inputMu sync.Mutex
91 var input string
92
93 render := func() {
94 inputMu.Lock()
95 masked := strings.Repeat("●", len([]rune(input)))
96 inputMu.Unlock()
97 e.RenderPanel(pid, "Password required",
98 []string{
99 " URL: " + in.URL,
100 "",
101 " Password: " + masked + "▌",
102 },
103 "type password enter confirm esc cancel")
104 }
105
106 e.OnPanelKey(pid, func(key, text string) {
107 inputMu.Lock()
108 switch key {
109 case "rune":
110 input += text
111 inputMu.Unlock()
112 render()
113 return
114 case "backspace":
115 if len(input) > 0 {
116 r := []rune(input)
117 input = string(r[:len(r)-1])
118 }
119 inputMu.Unlock()
120 render()
121 return
122 case "enter":
123 password := input
124 inputMu.Unlock()
125 e.ClosePanel(pid)
126 ch <- result{password: password, ok: true}
127 return
128 case "esc":
129 inputMu.Unlock()
130 e.ClosePanel(pid)
131 ch <- result{}
132 return
133 default:
134 inputMu.Unlock()
135 }
136 }, func() {
137 // Panel closed by host (user navigated away).
138 select {
139 case ch <- result{}:
140 default:
141 }
142 })
143
144 // Open the panel from inside the tool handler.
145 e.OpenPanel(pid, "Password required",
146 []string{
147 " URL: " + in.URL,
148 "",
149 " Password: ▌",
150 },
151 "type password enter confirm esc cancel")
152
153 // Block until the user submits or cancels.
154 r := <-ch
155 if !r.ok {
156 return ext.TextErrorResult("cancelled: user did not provide a password")
157 }
158
159 // Use the password directly here. It never leaves this
160 // process and is never written to any frame or transcript.
161 return doFetch(in.URL, r.password)
162 })
163
164 if err := e.Run(); err != nil {
165 e.Logf("fatal: %v", err)
166 os.Exit(1)
167 }
168}
169
170// doFetch is a stub. Replace with a real http.Get + BasicAuth or
171// token header in production use.
172func doFetch(url, password string) ext.ToolResult {
173 if password == "" {
174 return ext.TextErrorResult("fetch failed: empty password")
175 }
176 // Demonstrate that we *have* the password without logging it.
177 return ext.TextResult(fmt.Sprintf(
178 "fetched %s successfully (password was %d characters, not shown)",
179 url, len([]rune(password)),
180 ))
181}