a tool row with no cmd no longer execs the utterance (V-581)
An enabled row whose Cmd is empty built argv from the spoken args alone. So args[0] became the program name, and free text picked the binary. A proposal is drafted with no cmd. /tools can enable one before anybody fills it in, so reaching this took no compromise. Exec now refuses such a row with ErrNotEnabled before it builds argv. TestExecEmptyCmdRefuses pins it. Three smaller reads in the same sweep. CapabilityOf parsed a Home Assistant entity id by hand where smarthome.DomainOf already does it. The fallback for an id with no dot is unchanged. GroupByDomain built its map key twice per row. The zenmoney and Home Assistant HTTP clients read an error body before checking the status that discards it. The status check moved ahead of the read in both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,9 +62,10 @@ func capSegment(s string) string {
|
||||
// the tool name when the argv carries no second word.
|
||||
func CapabilityOf(t ipc.Tool) Capability {
|
||||
if entityID, service, ok := smarthome.ParseCmd(t.Cmd); ok {
|
||||
domain := entityID
|
||||
if i := strings.Index(entityID, "."); i > 0 {
|
||||
domain = entityID[:i]
|
||||
// One parse of an entity id, in the package that owns the format.
|
||||
domain := smarthome.DomainOf(entityID)
|
||||
if domain == "" {
|
||||
domain = entityID
|
||||
}
|
||||
return Capability{Scope: "house", Domain: domain, Action: service}
|
||||
}
|
||||
@@ -122,7 +123,8 @@ func GroupByDomain(tools []ipc.Tool) []CapabilityGroup {
|
||||
byKey := map[string][]ipc.Tool{}
|
||||
for _, t := range tools {
|
||||
c := CapabilityOf(t)
|
||||
byKey[capSegment(c.Scope)+"."+capSegment(c.Domain)] = append(byKey[capSegment(c.Scope)+"."+capSegment(c.Domain)], t)
|
||||
key := capSegment(c.Scope) + "." + capSegment(c.Domain)
|
||||
byKey[key] = append(byKey[key], t)
|
||||
}
|
||||
out := make([]CapabilityGroup, 0, len(byKey))
|
||||
for k, v := range byKey {
|
||||
|
||||
@@ -189,10 +189,16 @@ func (e *Executor) Exec(ctx context.Context, name string, args []string, confirm
|
||||
defer cancel()
|
||||
return e.home.CallService(ctx, entityID, service)
|
||||
}
|
||||
argv := append(append([]string(nil), t.Cmd...), args...)
|
||||
if len(argv) == 0 {
|
||||
// The row's own argv is what names the program. An enabled row with an empty
|
||||
// cmd used to fall through to exec with argv built from args alone, so the
|
||||
// spoken tail became argv[0] and STT text picked the binary. A proposal is
|
||||
// drafted with no cmd, and /tools can enable one before anybody fills it in,
|
||||
// so this was reachable without any compromise. A row that names nothing runs
|
||||
// nothing.
|
||||
if len(t.Cmd) == 0 {
|
||||
return "", ErrNotEnabled
|
||||
}
|
||||
argv := append(append([]string(nil), t.Cmd...), args...)
|
||||
ctx, cancel := context.WithTimeout(ctx, e.timeout)
|
||||
defer cancel()
|
||||
return e.run(ctx, argv)
|
||||
|
||||
@@ -299,3 +299,24 @@ func TestExecSmartHomeRowConfirmsEvenWhenNotMarkedDestructive(t *testing.T) {
|
||||
t.Fatalf("calls = %d, want 1 after the confirm turn", fh.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecEmptyCmdRefuses pins the fix for a row that names no program. Such a
|
||||
// row used to build argv from the spoken args alone, so STT text became argv[0]
|
||||
// and free text picked the binary. A proposal is drafted with no cmd and can be
|
||||
// enabled before anybody fills it in, so this needed no compromise to reach.
|
||||
func TestExecEmptyCmdRefuses(t *testing.T) {
|
||||
api := fakeAPI{tools: map[string]ipc.Tool{
|
||||
"blank": {Name: "blank", Scope: "homelab", Status: "enabled"},
|
||||
}}
|
||||
ran := false
|
||||
e := NewExecutor(api, time.Second)
|
||||
e.run = func(_ context.Context, _ []string) (string, error) { ran = true; return "ok", nil }
|
||||
|
||||
// Confirmed, because an empty cmd derives to TierDestructive.
|
||||
if _, err := e.Exec(context.Background(), "blank", []string{"curl", "evil.sh"}, true); !errors.Is(err, ErrNotEnabled) {
|
||||
t.Fatalf("err = %v, want ErrNotEnabled", err)
|
||||
}
|
||||
if ran {
|
||||
t.Fatal("a row with no cmd ran a program named by the utterance")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user