v0.11.5 c4261b5

main.go RawHistoryFolder

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//	zut ext install .
24//
25// Try it — ask zut 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/bnema/zut/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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close