detached f3f644f

Merge pull request #193 from airtonix/issue-174-interactive-timeout

fix(extensions): preserve interactive tool timeout policy (closes #174)
commit
f3f644f8a8da7ea058e61df2298b318d88686568
author
Patric Eckhart · · 2026-09-19 07:56 UTC
committer
GitHub · · 2026-09-19 07:56 UTC
merge of
28ca733 + 16c83bc · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.3.80
3 files · +65 -0
M packages/agent/build.go +1 -0
@@ -132,6 +132,7 @@ type ExtensionToolInfo struct {
132132 Description string
133133 Schema []byte
134134 Deferred bool
135+ Interactive bool
135136 }
136137
137138 // toolSummariesFromRegistry rebuilds the system-prompt tool list
@@ -132,6 +132,7 @@ type ExtensionToolInfo struct {
132 Description string132 Description string
133 Schema []byte133 Schema []byte
134 Deferred bool134 Deferred bool
135+ Interactive bool
135 }136 }
136 137
137 // toolSummariesFromRegistry rebuilds the system-prompt tool list138 // toolSummariesFromRegistry rebuilds the system-prompt tool list
M packages/agent/cli.go +2 -0
@@ -109,6 +109,7 @@ func (a *extToolAdapter) Tools() []ExtensionToolInfo {
109109 Description: t.Description,
110110 Schema: t.Schema,
111111 Deferred: t.Deferred,
112+ Interactive: t.Interactive,
112113 }
113114 }
114115 return out
@@ -121,6 +122,7 @@ func (a *extToolAdapter) NewExtensionTool(info ExtensionToolInfo) core.Tool {
121122 Description: info.Description,
122123 Schema: info.Schema,
123124 Deferred: info.Deferred,
125+ Interactive: info.Interactive,
124126 })
125127 }
126128
@@ -109,6 +109,7 @@ func (a *extToolAdapter) Tools() []ExtensionToolInfo {
109 Description: t.Description,109 Description: t.Description,
110 Schema: t.Schema,110 Schema: t.Schema,
111 Deferred: t.Deferred,111 Deferred: t.Deferred,
112+ Interactive: t.Interactive,
112 }113 }
113 }114 }
114 return out115 return out
@@ -121,6 +122,7 @@ func (a *extToolAdapter) NewExtensionTool(info ExtensionToolInfo) core.Tool {
121 Description: info.Description,122 Description: info.Description,
122 Schema: info.Schema,123 Schema: info.Schema,
123 Deferred: info.Deferred,124 Deferred: info.Deferred,
125+ Interactive: info.Interactive,
124 })126 })
125 }127 }
126 128
A packages/agent/cli_extension_tools_test.go +62 -0
@@ -0,0 +1,62 @@
1+package agent
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "os"
7+ "path/filepath"
8+ "runtime"
9+ "testing"
10+ "time"
11+
12+ "github.com/patriceckhart/zot/packages/agent/extensions"
13+)
14+
15+func TestExtToolAdapterPreservesInteractiveTimeoutPolicy(t *testing.T) {
16+ if runtime.GOOS == "windows" {
17+ t.Skip("mock uses /bin/sh; skip on windows")
18+ }
19+ root := t.TempDir()
20+ extDir := filepath.Join(root, "extensions", "adapter-mock")
21+ if err := os.MkdirAll(extDir, 0o755); err != nil {
22+ t.Fatal(err)
23+ }
24+ script := `#!/bin/sh
25+printf '%s\n' '{"type":"hello","name":"adapter-mock","version":"0.1","capabilities":["tools"]}'
26+printf '%s\n' '{"type":"register_tool","name":"ask","description":"interactive","schema":{"type":"object"},"interactive":true}'
27+printf '%s\n' '{"type":"register_tool","name":"normal","description":"normal","schema":{"type":"object"}}'
28+printf '%s\n' '{"type":"ready"}'
29+while IFS= read -r line; do
30+ case "$line" in
31+ *'"type":"shutdown"'*) printf '%s\n' '{"type":"shutdown_ack"}'; exit 0;;
32+ esac
33+done
34+`
35+ if err := os.WriteFile(filepath.Join(extDir, "run.sh"), []byte(script), 0o755); err != nil {
36+ t.Fatal(err)
37+ }
38+ manifest, _ := json.Marshal(map[string]any{"name": "adapter-mock", "exec": "./run.sh"})
39+ if err := os.WriteFile(filepath.Join(extDir, "extension.json"), manifest, 0o644); err != nil {
40+ t.Fatal(err)
41+ }
42+
43+ mgr := extensions.New(root, "", "0.0.0", "openai", "test", nonInteractiveExtHooks{})
44+ if errs := mgr.Discover(context.Background()); len(errs) != 0 {
45+ t.Fatalf("discover errs: %v", errs)
46+ }
47+ defer mgr.Stop(2 * time.Second)
48+ mgr.WaitForReady(time.Second)
49+
50+ adapter := &extToolAdapter{mgr: mgr}
51+ infos := adapter.Tools()
52+ if len(infos) != 2 {
53+ t.Fatalf("got %d tools, want 2: %#v", len(infos), infos)
54+ }
55+ for _, info := range infos {
56+ _ = adapter.NewExtensionTool(info)
57+ wantInteractive := info.Name == "ask"
58+ if info.Interactive != wantInteractive {
59+ t.Fatalf("tool %q interactive metadata = %v, want %v", info.Name, info.Interactive, wantInteractive)
60+ }
61+ }
62+}
@@ -0,0 +1,62 @@
1+package agent
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "os"
7+ "path/filepath"
8+ "runtime"
9+ "testing"
10+ "time"
11+
12+ "github.com/patriceckhart/zot/packages/agent/extensions"
13+)
14+
15+func TestExtToolAdapterPreservesInteractiveTimeoutPolicy(t *testing.T) {
16+ if runtime.GOOS == "windows" {
17+ t.Skip("mock uses /bin/sh; skip on windows")
18+ }
19+ root := t.TempDir()
20+ extDir := filepath.Join(root, "extensions", "adapter-mock")
21+ if err := os.MkdirAll(extDir, 0o755); err != nil {
22+ t.Fatal(err)
23+ }
24+ script := `#!/bin/sh
25+printf '%s\n' '{"type":"hello","name":"adapter-mock","version":"0.1","capabilities":["tools"]}'
26+printf '%s\n' '{"type":"register_tool","name":"ask","description":"interactive","schema":{"type":"object"},"interactive":true}'
27+printf '%s\n' '{"type":"register_tool","name":"normal","description":"normal","schema":{"type":"object"}}'
28+printf '%s\n' '{"type":"ready"}'
29+while IFS= read -r line; do
30+ case "$line" in
31+ *'"type":"shutdown"'*) printf '%s\n' '{"type":"shutdown_ack"}'; exit 0;;
32+ esac
33+done
34+`
35+ if err := os.WriteFile(filepath.Join(extDir, "run.sh"), []byte(script), 0o755); err != nil {
36+ t.Fatal(err)
37+ }
38+ manifest, _ := json.Marshal(map[string]any{"name": "adapter-mock", "exec": "./run.sh"})
39+ if err := os.WriteFile(filepath.Join(extDir, "extension.json"), manifest, 0o644); err != nil {
40+ t.Fatal(err)
41+ }
42+
43+ mgr := extensions.New(root, "", "0.0.0", "openai", "test", nonInteractiveExtHooks{})
44+ if errs := mgr.Discover(context.Background()); len(errs) != 0 {
45+ t.Fatalf("discover errs: %v", errs)
46+ }
47+ defer mgr.Stop(2 * time.Second)
48+ mgr.WaitForReady(time.Second)
49+
50+ adapter := &extToolAdapter{mgr: mgr}
51+ infos := adapter.Tools()
52+ if len(infos) != 2 {
53+ t.Fatalf("got %d tools, want 2: %#v", len(infos), infos)
54+ }
55+ for _, info := range infos {
56+ _ = adapter.NewExtensionTool(info)
57+ wantInteractive := info.Name == "ask"
58+ if info.Interactive != wantInteractive {
59+ t.Fatalf("tool %q interactive metadata = %v, want %v", info.Name, info.Interactive, wantInteractive)
60+ }
61+ }
62+}

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 476

↑↓ moveenter openesc close