34f6f1c786
Closes the cluster of audit findings where gnoma's incognito promise
('no persistence, no learning, local-only routing') silently broke
because state was duplicated across the CLI flag, the firewall's
IncognitoMode, the router's localOnly flag, and the TUI's local
m.incognito field. Wave 2 makes security.IncognitoMode the canonical
source of truth.
W2-1 Router.Select rejects forced non-local arms when localOnly is on
rather than short-circuiting and silently routing to cloud. Main
fails fast when --incognito + --provider <cloud> are combined; the
TUI toggle (Ctrl+X, /incognito, config panel) refuses with an
actionable message when a non-local arm is pinned. Factored the
three duplicated toggle sites into Model.attemptIncognitoToggle.
W2-2 persist.Store.Save consults an IncognitoGate (local interface,
*security.IncognitoMode satisfies it). nil gate = always persist
(legacy behaviour for tests); non-nil gate is consulted on every
Save so TUI runtime toggles take effect without reconstructing the
store. File mode 0o600, dir mode 0o700.
W2-3 tui.New seeds m.incognito from cfg.Firewall.Incognito().Active().
Fixes the Ctrl+X-on-launch-with-incognito case where the first
toggle silently turned the firewall OFF because the local flag
started false out of sync with the firewall.
W2-4 saveQuality gates on both *incognito (defensive, covers the
window before fwRef.Set fires) and fw.Incognito().ShouldLearn() (so
TUI Ctrl+X suppresses the snapshot on exit). Quality restore skipped
under --incognito. Quality file written 0o600 in dir 0o700.
engine.reportOutcome and elf.Manager.ReportResult both gate on
fw.Incognito().ShouldLearn() — bandit signal no longer leaks out of
incognito sessions.
W2-5 session files written 0o600 in dirs 0o700 (was 0o644 / 0o755).
W2-6 IncognitoMode.LocalOnly dropped — dead field with no readers;
routing local-only state lives on the router, not the firewall.
Also wires rtr.SetLocalOnly(true) when --incognito at launch — main
previously activated the firewall's flag but never told the router to
filter, so even without the forced-arm bug, launching with
--incognito alone gave you 'incognito badge but full arm pool'.
104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
package agent_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"somegit.dev/Owlibou/gnoma/internal/tool/agent"
|
|
"somegit.dev/Owlibou/gnoma/internal/tool/persist"
|
|
)
|
|
|
|
func makeTestStore(t *testing.T) *persist.Store {
|
|
t.Helper()
|
|
s := persist.New("test-coord-"+t.Name(), nil)
|
|
t.Cleanup(func() { _ = os.RemoveAll(s.Dir()) })
|
|
return s
|
|
}
|
|
|
|
func TestListResultsTool_EmptyStore(t *testing.T) {
|
|
s := makeTestStore(t)
|
|
tool := agent.NewListResultsTool(s)
|
|
args, _ := json.Marshal(map[string]string{})
|
|
result, err := tool.Execute(context.Background(), args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Empty store: either a "no results" message or empty output is acceptable;
|
|
// verify only that we don't surface a hard error.
|
|
if strings.Contains(result.Output, "error") {
|
|
t.Errorf("unexpected error output for empty store: %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestListResultsTool_ListsFiles(t *testing.T) {
|
|
s := makeTestStore(t)
|
|
big := strings.Repeat("x", 1024)
|
|
s.Save("bash", "toolu_aaa", big)
|
|
s.Save("fs.grep", "toolu_bbb", big)
|
|
|
|
tool := agent.NewListResultsTool(s)
|
|
args, _ := json.Marshal(map[string]string{})
|
|
result, err := tool.Execute(context.Background(), args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(result.Output, "bash") {
|
|
t.Errorf("expected bash in output, got: %s", result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "fs") {
|
|
t.Errorf("expected fs in output, got: %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestListResultsTool_FilterByToolName(t *testing.T) {
|
|
s := makeTestStore(t)
|
|
big := strings.Repeat("x", 1024)
|
|
s.Save("bash", "toolu_c1", big)
|
|
s.Save("fs.read", "toolu_c2", big)
|
|
|
|
tool := agent.NewListResultsTool(s)
|
|
args, _ := json.Marshal(map[string]string{"filter": "bash"})
|
|
result, err := tool.Execute(context.Background(), args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(result.Output, "bash") {
|
|
t.Errorf("filter should include bash, got: %s", result.Output)
|
|
}
|
|
if strings.Contains(result.Output, "fs") {
|
|
t.Errorf("filter should exclude fs.read, got: %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestReadResultTool_ReadsFile(t *testing.T) {
|
|
s := makeTestStore(t)
|
|
big := strings.Repeat("hello\n", 200)
|
|
path, _ := s.Save("bash", "toolu_read1", big)
|
|
|
|
tool := agent.NewReadResultTool(s)
|
|
args, _ := json.Marshal(map[string]string{"path": path})
|
|
result, err := tool.Execute(context.Background(), args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(result.Output, "hello") {
|
|
t.Errorf("expected file content in output, got: %s", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestReadResultTool_RejectsPathTraversal(t *testing.T) {
|
|
s := makeTestStore(t)
|
|
tool := agent.NewReadResultTool(s)
|
|
args, _ := json.Marshal(map[string]string{"path": "/etc/passwd"})
|
|
result, err := tool.Execute(context.Background(), args)
|
|
if err != nil {
|
|
t.Fatalf("Execute must not return a hard error; soft rejection in Output expected, got: %v", err)
|
|
}
|
|
if !strings.Contains(result.Output, "outside") {
|
|
t.Errorf("expected 'outside session directory' rejection, got: %s", result.Output)
|
|
}
|
|
}
|