diff --git a/SESSION-05-07-2026.md b/SESSION-05-07-2026.md index fe06f2c..5dc2d27 100644 --- a/SESSION-05-07-2026.md +++ b/SESSION-05-07-2026.md @@ -31,7 +31,7 @@ Notes: | # | Task | Commit | Status | |---|------|--------|--------| | 5 | **mavcaldav tests** — `cmd/mavcaldav/` (314 lines, 0 coverage). CalDAV polling, iCal parsing, value-change filtering | 6daa96b | done | -| 6 | **mavttsd tests** — `cmd/mavttsd/` (Piper handler). TTS worker protocol round-trip | — | pending | +| 6 | **mavttsd tests** — `cmd/mavttsd/` (Piper handler). TTS worker protocol round-trip | a80b919 | done | | 7 | **voicesink tests** — `internal/delivery/voicesink/`. Voice channel dispatch, ErrNoSession mapping | ffef44f | done | | 8 | **mavweb tests** — extend to cover `main.go` routes (server setup, template parsing, route registration, startup flags) | 185f4f5 | done | diff --git a/cmd/mavend/voice.go b/cmd/mavend/voice.go index 7792a6f..7bb646b 100644 --- a/cmd/mavend/voice.go +++ b/cmd/mavend/voice.go @@ -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 } diff --git a/cmd/mavweb/handlers_test.go b/cmd/mavweb/handlers_test.go index 87bd8f1..a6eb167 100644 --- a/cmd/mavweb/handlers_test.go +++ b/cmd/mavweb/handlers_test.go @@ -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: "x", Utterance: "restart the thing"}}, - enabled: []ipc.Tool{{Name: "svc", Cmd: []string{"systemctl", "restart"}, Destructive: true}}, + proposed: []ipc.Tool{{Name: "x", Scope: "homelab", Utterance: "restart the 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) diff --git a/cmd/mavweb/main.go b/cmd/mavweb/main.go index 422262a..efd0f1c 100644 --- a/cmd/mavweb/main.go +++ b/cmd/mavweb/main.go @@ -344,11 +344,12 @@ input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem} {{if .Msg}}
{{.Msg}}
{{end}}

proposed ({{len .Proposed}})

{{if .Proposed}}

maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable.

- +
namefrom utteranceenable as
{{range .Proposed}} - +{{end}}
namescopefrom utteranceenable as
{{.Name}}{{.Utterance}}{{.Name}}{{.Scope}}{{.Utterance}}
+ @@ -356,11 +357,12 @@ input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem}
{{else}}

none pending.

{{end}}

enabled ({{len .Enabled}})

-{{if .Enabled}} -{{range .Enabled}} +{{if .Enabled}}
namecommand
{{.Name}}{{join .Cmd " "}}
+{{range .Enabled}}{{end}}
namescopecommand
{{.Name}}{{.Scope}}{{join .Cmd " "}} {{if .Destructive}}destructive{{end}} +
{{else}}

none enabled.

{{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 diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 875ffce..85b5466 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -363,10 +363,10 @@ func (r *recordingAPI) QueryNotes(_ context.Context, _ []float32, _ int) ([]ipc. func (r *recordingAPI) RecentNotes(_ context.Context, _ int) ([]ipc.Note, error) { return nil, nil } -func (r *recordingAPI) ProposeTool(_ context.Context, _, _ string, _ time.Time) (bool, error) { +func (r *recordingAPI) ProposeTool(_ context.Context, _, _, _ string, _ time.Time) (bool, error) { return false, nil } -func (r *recordingAPI) EnableTool(_ context.Context, _ string, _ []string, _ bool, _ time.Time) error { +func (r *recordingAPI) EnableTool(_ context.Context, _ string, _ []string, _ bool, _ string, _ time.Time) error { return nil } func (r *recordingAPI) DisableTool(_ context.Context, _ string) error { diff --git a/internal/config/config.go b/internal/config/config.go index 855559d..8d20da6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -180,6 +180,7 @@ type VoiceConfig struct { // must not fire from the voice path (they need a confirm on an authed surface). type ToolConfig struct { Name string `json:"name"` + Scope string `json:"scope,omitempty"` Cmd []string `json:"cmd"` Destructive bool `json:"destructive,omitempty"` } diff --git a/internal/ipc/api.go b/internal/ipc/api.go index 4b82e90..01a5708 100644 --- a/internal/ipc/api.go +++ b/internal/ipc/api.go @@ -157,6 +157,7 @@ type sinceResp struct { // inert scaffold; 'enabled' is runnable. The executor only runs 'enabled'. type Tool struct { Name string `json:"name"` + Scope string `json:"scope"` Cmd []string `json:"cmd"` Destructive bool `json:"destructive"` Status string `json:"status"` @@ -167,6 +168,7 @@ type Tool struct { type proposeToolReq struct { Name string `json:"name"` + Scope string `json:"scope"` Utterance string `json:"utterance"` Ts time.Time `json:"ts"` } @@ -175,6 +177,7 @@ type proposeToolResp struct { } type enableToolReq struct { Name string `json:"name"` + Scope string `json:"scope"` Cmd []string `json:"cmd"` Destructive bool `json:"destructive"` Ts time.Time `json:"ts"` @@ -227,8 +230,9 @@ type CoreAPI interface { // Enable/DisableTool gate at AuthStepUp (allowlist mutation, human-only); // ProposeTool is maven-callable (no step-up — she has no passkey). // LookupTool/ListTools read them. - ProposeTool(ctx context.Context, name, utterance string, ts time.Time) (bool, error) - EnableTool(ctx context.Context, name string, cmd []string, destructive bool, ts time.Time) error + // scope defaults to "homelab" when empty. + ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) + EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error DisableTool(ctx context.Context, name string) error LookupTool(ctx context.Context, name string) (Tool, error) ListTools(ctx context.Context, status string) ([]Tool, error) diff --git a/internal/ipc/client.go b/internal/ipc/client.go index 1633f7c..e9aab0f 100644 --- a/internal/ipc/client.go +++ b/internal/ipc/client.go @@ -309,16 +309,16 @@ func (c *Client) RecentNotes(ctx context.Context, n int) ([]Note, error) { return out, nil } -func (c *Client) ProposeTool(ctx context.Context, name, utterance string, ts time.Time) (bool, error) { +func (c *Client) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) { var r proposeToolResp - if err := c.call(ctx, MethodProposeTool, proposeToolReq{Name: name, Utterance: utterance, Ts: ts}, &r); err != nil { + if err := c.call(ctx, MethodProposeTool, proposeToolReq{Name: name, Scope: scope, Utterance: utterance, Ts: ts}, &r); err != nil { return false, err } return r.Proposed, nil } -func (c *Client) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, ts time.Time) error { - return c.call(ctx, MethodEnableTool, enableToolReq{Name: name, Cmd: cmd, Destructive: destructive, Ts: ts}, nil) +func (c *Client) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error { + return c.call(ctx, MethodEnableTool, enableToolReq{Name: name, Scope: scope, Cmd: cmd, Destructive: destructive, Ts: ts}, nil) } func (c *Client) DisableTool(ctx context.Context, name string) error { diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 9a73f73..52c3300 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -144,13 +144,13 @@ func (a *storeAPI) RecentNotes(ctx context.Context, n int) ([]Note, error) { return out, nil } -func (a *storeAPI) ProposeTool(ctx context.Context, name, utterance string, ts time.Time) (bool, error) { - ok, err := a.s.ProposeTool(ctx, name, utterance, ts) +func (a *storeAPI) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) { + ok, err := a.s.ProposeTool(ctx, name, utterance, scope, ts) return ok, mapErr(err) } -func (a *storeAPI) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, ts time.Time) error { - return mapErr(a.s.EnableTool(ctx, name, cmd, destructive, ts)) +func (a *storeAPI) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error { + return mapErr(a.s.EnableTool(ctx, name, cmd, destructive, scope, ts)) } func (a *storeAPI) DisableTool(ctx context.Context, name string) error { @@ -184,8 +184,8 @@ func (a *storeAPI) ListTools(ctx context.Context, status string) ([]Tool, error) func toTool(t store.Tool) Tool { return Tool{ - Name: t.Name, Cmd: t.Cmd, Destructive: t.Destructive, Status: t.Status, - Utterance: t.Utterance, Created: t.CreatedTs, Updated: t.UpdatedTs, + Name: t.Name, Scope: t.Scope, Cmd: t.Cmd, Destructive: t.Destructive, + Status: t.Status, Utterance: t.Utterance, Created: t.CreatedTs, Updated: t.UpdatedTs, } } @@ -567,7 +567,7 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er if err := unmarshalParams(req.Params, &p); err != nil { return nil, err } - ok, err := s.api.ProposeTool(ctx, p.Name, p.Utterance, p.Ts) + ok, err := s.api.ProposeTool(ctx, p.Name, p.Utterance, p.Scope, p.Ts) if err != nil { return nil, err } @@ -578,7 +578,7 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er if err := unmarshalParams(req.Params, &p); err != nil { return nil, err } - return marshalResult(nil), s.api.EnableTool(ctx, p.Name, p.Cmd, p.Destructive, p.Ts) + return marshalResult(nil), s.api.EnableTool(ctx, p.Name, p.Cmd, p.Destructive, p.Scope, p.Ts) case MethodDisableTool: var p disableToolReq diff --git a/internal/store/migrations.go b/internal/store/migrations.go index 2b28c22..4c3e8fe 100644 --- a/internal/store/migrations.go +++ b/internal/store/migrations.go @@ -16,7 +16,9 @@ import ( // var migrations = []string{ // `ALTER TABLE ...;`, // #1 // } -var migrations = []string{} +var migrations = []string{ + `ALTER TABLE tools ADD COLUMN scope TEXT NOT NULL DEFAULT 'homelab';`, +} // migrate applies every migration with a number greater than the DB's current // user_version, each in its own transaction that also bumps user_version. Fails diff --git a/internal/store/migrations_test.go b/internal/store/migrations_test.go index 9a2a6bd..e561413 100644 --- a/internal/store/migrations_test.go +++ b/internal/store/migrations_test.go @@ -18,20 +18,22 @@ func TestMigrateAppliesOnceAndIsIdempotent(t *testing.T) { ctx := context.Background() s := newTestStore(t) - // Empty baseline slice leaves the DB at version 0. - if v := userVersion(t, s); v != 0 { - t.Fatalf("fresh DB user_version = %d, want 0", v) + // The 1 migration in the built-in slice (tools scope) was applied on Open. + startVer := len(migrations) + if v := userVersion(t, s); v != startVer { + t.Fatalf("fresh DB user_version = %d, want %d", v, startVer) } - // Append a fake migration and run it: creates a throwaway table, bumps to 1. + // Append a fake migration and run it: creates a throwaway table, bumps by 1. migrations = append(migrations, `CREATE TABLE migrate_probe (id INTEGER PRIMARY KEY)`) t.Cleanup(func() { migrations = migrations[:len(migrations)-1] }) if err := migrate(ctx, s.db); err != nil { t.Fatalf("migrate: %v", err) } - if v := userVersion(t, s); v != 1 { - t.Fatalf("after migrate user_version = %d, want 1", v) + want := startVer + 1 + if v := userVersion(t, s); v != want { + t.Fatalf("after migrate user_version = %d, want %d", v, want) } if _, err := s.db.ExecContext(ctx, "INSERT INTO migrate_probe DEFAULT VALUES"); err != nil { t.Fatalf("probe table not created: %v", err) @@ -41,7 +43,7 @@ func TestMigrateAppliesOnceAndIsIdempotent(t *testing.T) { if err := migrate(ctx, s.db); err != nil { t.Fatalf("migrate second run not idempotent: %v", err) } - if v := userVersion(t, s); v != 1 { - t.Fatalf("after re-migrate user_version = %d, want 1", v) + if v := userVersion(t, s); v != want { + t.Fatalf("after re-migrate user_version = %d, want %d", v, want) } } diff --git a/internal/store/tools.go b/internal/store/tools.go index 8bb02b9..5d7d2a8 100644 --- a/internal/store/tools.go +++ b/internal/store/tools.go @@ -9,11 +9,13 @@ import ( "time" ) -// Tool — one act in the allowlist. Cmd is the fixed argv prefix run with the -// utterance's args appended (no shell). Status 'proposed' is a scaffold that -// drives nothing; 'enabled' is the human-flipped, runnable form. +// Tool — one act in the allowlist. Scope namespaces tools (e.g. "homelab"). +// Cmd is the fixed argv prefix run with the utterance's args appended (no +// shell). Status 'proposed' is a scaffold that drives nothing; 'enabled' is +// the human-flipped, runnable form. type Tool struct { Name string + Scope string Cmd []string Destructive bool Status string // proposed | enabled @@ -34,12 +36,16 @@ var ( // false when a row (proposed or enabled) already existed. maven calls this when // she classifies an act whose verb isn't on the enabled allowlist — she drafts // the registration; a human enables it. Never overwrites an enabled tool. -func (s *Store) ProposeTool(ctx context.Context, name, utterance string, ts time.Time) (bool, error) { +// scope defaults to "homelab" when empty. +func (s *Store) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) { + if scope == "" { + scope = "homelab" + } res, err := s.db.ExecContext(ctx, ` - INSERT INTO tools (name, cmd, destructive, status, utterance, created_ts, updated_ts) - VALUES (?, '[]', 0, 'proposed', ?, ?, ?) + INSERT INTO tools (name, scope, cmd, destructive, status, utterance, created_ts, updated_ts) + VALUES (?, ?, '[]', 0, 'proposed', ?, ?, ?) ON CONFLICT(name) DO NOTHING`, - name, utterance, ts.UnixMilli(), ts.UnixMilli()) + name, scope, utterance, ts.UnixMilli(), ts.UnixMilli()) if err != nil { return false, fmt.Errorf("propose tool: %w", err) } @@ -54,10 +60,14 @@ func (s *Store) ProposeTool(ctx context.Context, name, utterance string, ts time // human "enable" act (the authed surface calls it); it upserts so enabling a // name that was never proposed still works. An empty cmd is refused — an // enabled tool that runs nothing is a footgun, not a tool. -func (s *Store) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, ts time.Time) error { +// scope defaults to "homelab" when empty. +func (s *Store) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error { if len(cmd) == 0 { return ErrToolCmd } + if scope == "" { + scope = "homelab" + } raw, err := json.Marshal(cmd) if err != nil { return fmt.Errorf("enable tool: %w", err) @@ -67,11 +77,11 @@ func (s *Store) EnableTool(ctx context.Context, name string, cmd []string, destr d = 1 } _, err = s.db.ExecContext(ctx, ` - INSERT INTO tools (name, cmd, destructive, status, utterance, created_ts, updated_ts) - VALUES (?, ?, ?, 'enabled', '', ?, ?) - ON CONFLICT(name) DO UPDATE SET cmd=excluded.cmd, destructive=excluded.destructive, + INSERT INTO tools (name, scope, cmd, destructive, status, utterance, created_ts, updated_ts) + VALUES (?, ?, ?, ?, 'enabled', '', ?, ?) + ON CONFLICT(name) DO UPDATE SET scope=excluded.scope, cmd=excluded.cmd, destructive=excluded.destructive, status='enabled', updated_ts=excluded.updated_ts`, - name, string(raw), d, ts.UnixMilli(), ts.UnixMilli()) + name, scope, string(raw), d, ts.UnixMilli(), ts.UnixMilli()) if err != nil { return fmt.Errorf("enable tool: %w", err) } @@ -95,7 +105,7 @@ func (s *Store) DisableTool(ctx context.Context, name string) error { // LookupTool returns the tool by name. ErrToolNotFound when absent. func (s *Store) LookupTool(ctx context.Context, name string) (Tool, error) { row := s.db.QueryRowContext(ctx, ` - SELECT name, cmd, destructive, status, utterance, created_ts, updated_ts + SELECT name, scope, cmd, destructive, status, utterance, created_ts, updated_ts FROM tools WHERE name = ?`, name) t, err := scanTool(row) if errors.Is(err, sql.ErrNoRows) { @@ -106,7 +116,7 @@ func (s *Store) LookupTool(ctx context.Context, name string) (Tool, error) { // ListTools returns tools filtered by status ("" ⇒ all), name-sorted. func (s *Store) ListTools(ctx context.Context, status string) ([]Tool, error) { - q := `SELECT name, cmd, destructive, status, utterance, created_ts, updated_ts FROM tools` + q := `SELECT name, scope, cmd, destructive, status, utterance, created_ts, updated_ts FROM tools` var args []any if status != "" { q += ` WHERE status = ?` @@ -137,7 +147,7 @@ func scanTool(sc scanner) (Tool, error) { var cmdJSON string var d int var created, updated int64 - if err := sc.Scan(&t.Name, &cmdJSON, &d, &t.Status, &t.Utterance, &created, &updated); err != nil { + if err := sc.Scan(&t.Name, &t.Scope, &cmdJSON, &d, &t.Status, &t.Utterance, &created, &updated); err != nil { return Tool{}, err } if err := json.Unmarshal([]byte(cmdJSON), &t.Cmd); err != nil { diff --git a/internal/store/tools_test.go b/internal/store/tools_test.go index 27cc8b6..1111e6b 100644 --- a/internal/store/tools_test.go +++ b/internal/store/tools_test.go @@ -14,15 +14,32 @@ func TestToolLifecycle(t *testing.T) { ctx := context.Background() now := time.Now() - if _, err := s.ProposeTool(ctx, "restart_svc", "restart the service", now); err != nil { + // Propose with empty scope → defaults to "homelab". + if _, err := s.ProposeTool(ctx, "restart_svc", "restart the service", "", now); err != nil { t.Fatalf("propose: %v", err) } - if err := s.EnableTool(ctx, "restart_svc", []string{"systemctl", "restart", "x"}, true, now); err != nil { + if err := s.EnableTool(ctx, "restart_svc", []string{"systemctl", "restart", "x"}, true, "", now); err != nil { t.Fatalf("enable: %v", err) } - if tl, _ := s.LookupTool(ctx, "restart_svc"); tl.Status != "enabled" { + tl, _ := s.LookupTool(ctx, "restart_svc") + if tl.Status != "enabled" { t.Fatalf("after enable: status=%q want enabled", tl.Status) } + if tl.Scope != "homelab" { + t.Fatalf("after enable: scope=%q want homelab", tl.Scope) + } + + // Propose with explicit scope. + if _, err := s.ProposeTool(ctx, "reboot", "reboot the server", "datacenter", now); err != nil { + t.Fatalf("propose with scope: %v", err) + } + if err := s.EnableTool(ctx, "reboot", []string{"reboot"}, true, "datacenter", now); err != nil { + t.Fatalf("enable with scope: %v", err) + } + tl2, _ := s.LookupTool(ctx, "reboot") + if tl2.Scope != "datacenter" { + t.Fatalf("explicit scope: %q want datacenter", tl2.Scope) + } if err := s.DisableTool(ctx, "restart_svc"); err != nil { t.Fatalf("disable: %v", err) diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 24478b1..d89a8ac 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -39,7 +39,7 @@ import ( type API interface { LookupTool(ctx context.Context, name string) (ipc.Tool, error) ListTools(ctx context.Context, status string) ([]ipc.Tool, error) - ProposeTool(ctx context.Context, name, utterance string, ts time.Time) (bool, error) + ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) } var ( diff --git a/internal/tool/tool_test.go b/internal/tool/tool_test.go index d184258..a28b4bb 100644 --- a/internal/tool/tool_test.go +++ b/internal/tool/tool_test.go @@ -29,7 +29,7 @@ func (f fakeAPI) ListTools(_ context.Context, status string) ([]ipc.Tool, error) } return out, nil } -func (f fakeAPI) ProposeTool(_ context.Context, _, _ string, _ time.Time) (bool, error) { +func (f fakeAPI) ProposeTool(_ context.Context, _, _, _ string, _ time.Time) (bool, error) { return true, nil } @@ -37,9 +37,9 @@ func (f fakeAPI) ProposeTool(_ context.Context, _, _ string, _ time.Time) (bool, // destructive needs confirm, and args land as argv (no shell) after the prefix. func TestExec(t *testing.T) { api := fakeAPI{tools: map[string]ipc.Tool{ - "restart": {Name: "restart", Cmd: []string{"systemctl", "restart"}, Status: "enabled"}, - "drop": {Name: "drop", Cmd: []string{"dropdb"}, Destructive: true, Status: "enabled"}, - "draft": {Name: "draft", Cmd: []string{"x"}, Status: "proposed"}, + "restart": {Name: "restart", Scope: "homelab", Cmd: []string{"systemctl", "restart"}, Status: "enabled"}, + "drop": {Name: "drop", Scope: "homelab", Cmd: []string{"dropdb"}, Destructive: true, Status: "enabled"}, + "draft": {Name: "draft", Scope: "homelab", Cmd: []string{"x"}, Status: "proposed"}, }} var gotArgv []string