tools: add scope column for capability model

Add a 'scope' TEXT column (default 'homelab') to the tools table so tools
can be namespaced by scope (e.g. "homelab:restart", "datacenter:reboot").
Backward-compat: bare name defaults to "homelab" scope.

Changes:
- Migration #1: ALTER TABLE tools ADD COLUMN scope
- store.Tool: add Scope field, update all SQL and scanTool()
- ipc.Tool DTO and request types: add Scope field
- CoreAPI interface: pass scope in ProposeTool/EnableTool
- storeAPI adapters: forward scope
- cmd/mavend/voice: pass scope (empty → homelab)
- cmd/mavweb/tools: show scope column in UI tables, hidden fields
- All tests updated for scope field
- Migration test made dynamic (startVer = len(migrations))
This commit is contained in:
kami
2026-07-05 11:40:15 +04:00
parent a80b919780
commit 6b80fd0c0f
15 changed files with 98 additions and 59 deletions
+2 -2
View File
@@ -719,7 +719,7 @@ func (h *reactiveHandler) proposeGap(ctx context.Context, dec router.Decision) s
if name == "" {
return "не разобрала команду — попробуй иначе."
}
newly, err := h.api.ProposeTool(ctx, name, dec.Utterance, h.now())
newly, err := h.api.ProposeTool(ctx, name, dec.Utterance, "", h.now())
if err != nil {
log.Printf("voice: propose tool %q: %v", name, err)
return "команды «" + name + "» нет в списке разрешённых."
@@ -800,7 +800,7 @@ func seedTools(api ipc.CoreAPI, tools []config.ToolConfig) {
log.Printf("voice: skipping malformed tool config %+v", tc)
continue
}
if err := api.EnableTool(ctx, tc.Name, tc.Cmd, tc.Destructive, now); err != nil {
if err := api.EnableTool(ctx, tc.Name, tc.Cmd, tc.Destructive, tc.Scope, now); err != nil {
log.Printf("voice: seed tool %q: %v", tc.Name, err)
continue
}
+3 -3
View File
@@ -56,7 +56,7 @@ type fakeCore struct {
historyErr error
}
func (f *fakeCore) EnableTool(_ context.Context, name string, cmd []string, destructive bool, _ time.Time) error {
func (f *fakeCore) EnableTool(_ context.Context, name string, cmd []string, destructive bool, scope string, _ time.Time) error {
f.gotEnableName, f.gotEnableCmd, f.gotEnableDest = name, cmd, destructive
return f.enableErr
}
@@ -134,8 +134,8 @@ func (f *fakeCore) RevertFact(_ context.Context, _ string) (int64, error) {
func TestHandleTools_GET_RendersAndEscapes(t *testing.T) {
core := &fakeCore{
proposed: []ipc.Tool{{Name: "<b>x", Utterance: "restart the <i>thing"}},
enabled: []ipc.Tool{{Name: "svc", Cmd: []string{"systemctl", "restart"}, Destructive: true}},
proposed: []ipc.Tool{{Name: "<b>x", Scope: "homelab", Utterance: "restart the <i>thing"}},
enabled: []ipc.Tool{{Name: "svc", Scope: "", Cmd: []string{"systemctl", "restart"}, Destructive: true}},
}
rr := httptest.NewRecorder()
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), core)
+8 -5
View File
@@ -344,11 +344,12 @@ input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem}
{{if .Msg}}<div class=msg>{{.Msg}}</div>{{end}}
<h2>proposed <small>({{len .Proposed}})</small></h2>
{{if .Proposed}}<p>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable.</p>
<table><tr><th>name</th><th>from utterance</th><th>enable as</th></tr>
<table><tr><th>name</th><th>scope</th><th>from utterance</th><th>enable as</th></tr>
{{range .Proposed}}<tr>
<td><code>{{.Name}}</code></td><td>{{.Utterance}}</td>
<td><code>{{.Name}}</code></td><td>{{.Scope}}</td><td>{{.Utterance}}</td>
<td><form method=post action=/tools>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=scope value="{{.Scope}}">
<input type=hidden name=action value=enable>
<input type=text name=cmd placeholder="systemctl restart" required>
<label><input type=checkbox name=destructive> destructive</label>
@@ -356,11 +357,12 @@ input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem}
</tr>{{end}}</table>
{{else}}<p>none pending.</p>{{end}}
<h2>enabled <small>({{len .Enabled}})</small></h2>
{{if .Enabled}}<table><tr><th>name</th><th>command</th><th></th><th></th></tr>
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td><code>{{join .Cmd " "}}</code></td>
{{if .Enabled}}<table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td>{{.Scope}}</td><td><code>{{join .Cmd " "}}</code></td>
<td>{{if .Destructive}}<span class=d>destructive</span>{{end}}</td>
<td><form method=post action=/tools style=display:inline>
<input type=hidden name=name value="{{.Name}}">
<input type=hidden name=scope value="{{.Scope}}">
<input type=hidden name=action value=disable>
<button>disable</button></form></td></tr>{{end}}</table>
{{else}}<p>none enabled.</p>{{end}}
@@ -435,13 +437,14 @@ func handleTools(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
name := strings.TrimSpace(r.FormValue("name"))
switch action {
case "enable":
scope := r.FormValue("scope")
cmd := strings.Fields(r.FormValue("cmd"))
destructive := r.FormValue("destructive") != ""
if name == "" || len(cmd) == 0 {
http.Error(w, "name and cmd required", http.StatusBadRequest)
return
}
if err := core.EnableTool(ctx, name, cmd, destructive, time.Now()); err != nil {
if err := core.EnableTool(ctx, name, cmd, destructive, scope, time.Now()); err != nil {
log.Printf("tools: enable %q: %v", name, err)
http.Error(w, "enable failed: "+err.Error(), http.StatusBadGateway)
return