fix: append mutation, pipe-mode hang, Mistral regex false positives
- Fix append footgun: allHooks/allMCPServers allocated fresh to avoid
mutating cfg's backing array (lines 391/413 in main.go)
- Fix pipe-mode permission prompt: detect no-TTY stdin and auto-deny
instead of blocking forever on fmt.Scanln EOF
- Tighten Mistral API key regex from bare [a-zA-Z0-9]{32} (matched
commit hashes, UUIDs) to context-gated pattern requiring "mistral"
keyword nearby. Added scanner test for positives and negatives.
- Remove README demo GIF TODO placeholder
- Unify version string: pass buildVersion from ldflags into tui.Config
instead of hardcoding "v0.1.0-dev"
- Populate benchmarks doc with actual Go benchmark results
This commit is contained in:
@@ -2,9 +2,6 @@
|
||||
|
||||
**A provider-agnostic agentic coding assistant built in Go.** gnoma routes tasks to the best available LLM — cloud or local — through a multi-armed bandit router, while tools, hooks, skills, MCP servers, and plugins keep it extensible. Named after the northern pygmy-owl (*Glaucidium gnoma*); agents are called **elfs** (elf owl).
|
||||
|
||||
<!-- TODO: replace with actual demo recording -->
|
||||
<!--  -->
|
||||
|
||||
## Quickstart
|
||||
|
||||
```sh
|
||||
|
||||
@@ -325,8 +325,14 @@ func main() {
|
||||
logger.Debug("incognito mode enabled")
|
||||
}
|
||||
|
||||
// Permission checker with console prompt for pipe mode
|
||||
// Permission checker with console prompt for pipe mode.
|
||||
// In pure pipe mode (no TTY on stdin), auto-deny — Scanln would block on EOF.
|
||||
pipePromptFn := func(ctx context.Context, toolName string, args json.RawMessage) (bool, error) {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if stat.Mode()&os.ModeCharDevice == 0 {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Tool %s denied (no TTY for prompt, use --permission bypass to allow)\n", toolName)
|
||||
return false, nil
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "⚠ Tool %s wants to execute. Allow? [y/N] ", toolName)
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
@@ -388,7 +394,9 @@ func main() {
|
||||
// Build hook dispatcher from config + plugin hooks.
|
||||
// Streamer adapter wraps the router for prompt hooks.
|
||||
// ElfSpawnFn closure wraps elfMgr for agent hooks.
|
||||
allHooks := append(cfg.Hooks, pluginResult.Hooks...)
|
||||
allHooks := make([]gnomacfg.HookConfig, 0, len(cfg.Hooks)+len(pluginResult.Hooks))
|
||||
allHooks = append(allHooks, cfg.Hooks...)
|
||||
allHooks = append(allHooks, pluginResult.Hooks...)
|
||||
hookDefs, err := hook.ParseHookDefs(allHooks)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "hook config error: %v\n", err)
|
||||
@@ -410,7 +418,9 @@ func main() {
|
||||
}
|
||||
|
||||
// Start MCP servers (config + plugin) and register tools in the tool registry.
|
||||
allMCPServers := append(cfg.MCPServers, pluginResult.MCPServers...)
|
||||
allMCPServers := make([]gnomacfg.MCPServerConfig, 0, len(cfg.MCPServers)+len(pluginResult.MCPServers))
|
||||
allMCPServers = append(allMCPServers, cfg.MCPServers...)
|
||||
allMCPServers = append(allMCPServers, pluginResult.MCPServers...)
|
||||
var mcpMgr *mcp.Manager
|
||||
if len(allMCPServers) > 0 {
|
||||
serverCfgs, err := mcp.ParseServerConfigs(allMCPServers)
|
||||
@@ -665,6 +675,7 @@ func main() {
|
||||
StartWithResumePicker: openResumePicker,
|
||||
Skills: skillReg,
|
||||
PluginInfos: buildPluginInfos(discoveredPlugins, enabledSet),
|
||||
Version: buildVersion,
|
||||
})
|
||||
p := tea.NewProgram(m)
|
||||
if _, err := p.Run(); err != nil {
|
||||
|
||||
@@ -31,11 +31,26 @@ go test -bench=. -benchmem ./internal/router/
|
||||
go run ./cmd/gnoma-bench/ --arms=5 --tasks=1000 --seed=42
|
||||
```
|
||||
|
||||
## Results
|
||||
## Results (M4 heuristic, 2026-04-12)
|
||||
|
||||
No benchmark results yet. This scaffold will be populated as M9 (Router Advanced) lands.
|
||||
5 arms (Sonnet, Opus, GPT-4o, Qwen3:8b local, Mistral Large), 10 task types. AMD Ryzen 7 3700X.
|
||||
|
||||
### Planned comparisons
|
||||
```
|
||||
BenchmarkScoreArm-16 3046383 392.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSelectBest-16 276529 4347 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkFilterFeasible-16 1200006 1003 ns/op 504 B/op 10 allocs/op
|
||||
BenchmarkRouterSelect-16 177916 6794 ns/op 1224 B/op 40 allocs/op
|
||||
BenchmarkRouterSelectWithQuality-16 152180 7885 ns/op 1224 B/op 40 allocs/op
|
||||
BenchmarkClassifyTask-16 122278 9780 ns/op 1536 B/op 14 allocs/op
|
||||
```
|
||||
|
||||
Key observations:
|
||||
- `ScoreArm` is zero-alloc at ~400ns — good headroom for M9 bandit sampling overhead
|
||||
- Full `Select` (filter + score + pool reserve + commit) is ~7us per routing decision
|
||||
- Quality tracker adds ~1us overhead (7.9us vs 6.8us) — acceptable for EMA lookups
|
||||
- `ClassifyTask` at ~10us is dominated by `strings.Contains` keyword matching; a trie or compiled regex could reduce this if it becomes a bottleneck, but for per-request overhead it's negligible
|
||||
|
||||
### Planned comparisons (M9)
|
||||
|
||||
- Heuristic-only (M4) vs. bandit (M9) after 50, 200, 1000 observations
|
||||
- 2-arm (local + cloud) vs. 5-arm (mixed providers) scenarios
|
||||
|
||||
@@ -175,7 +175,7 @@ func defaultPatterns() []SecretPattern {
|
||||
{"openai_api_key", `sk-(?:proj-)?[a-zA-Z0-9_-]{20,}`},
|
||||
{"openai_svcacct_key", `sk-svcacct-[a-zA-Z0-9_-]{20,}`},
|
||||
{"openai_admin_key", `sk-admin-[a-zA-Z0-9_-]{20,}`},
|
||||
{"mistral_api_key", `[a-zA-Z0-9]{32}(?:[a-zA-Z0-9]{0})`}, // 32-char; entropy-gated
|
||||
{"mistral_api_key", `(?i)(?:mistral|MISTRAL)[_\s]*(?:api[_\s]*)?key[=:\s"']+([a-zA-Z0-9]{32})\b`}, // context-gated: requires "mistral" nearby
|
||||
{"huggingface_token", `hf_[a-zA-Z0-9]{34,}`},
|
||||
|
||||
// --- Cloud Providers ---
|
||||
|
||||
@@ -99,6 +99,45 @@ func TestScanner_DetectsDatabaseURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_DetectsMistralKey(t *testing.T) {
|
||||
s := NewScanner(6.0)
|
||||
|
||||
// Should detect Mistral key in assignment contexts.
|
||||
positives := []string{
|
||||
`MISTRAL_API_KEY=abcdefghijklmnop1234567890abcdef`,
|
||||
`mistral_key = "abcdefghijklmnop1234567890abcdef"`,
|
||||
`MISTRAL_API_KEY: abcdefghijklmnop1234567890abcdef`,
|
||||
`export MISTRAL_API_KEY='abcdefghijklmnop1234567890abcdef'`,
|
||||
}
|
||||
for _, text := range positives {
|
||||
matches := s.Scan(text)
|
||||
found := false
|
||||
for _, m := range matches {
|
||||
if m.Pattern == "mistral_api_key" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("should detect Mistral key in: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
// Should NOT false-positive on bare 32-char strings.
|
||||
negatives := []string{
|
||||
`commit abcdefabcdefabcdefabcdefabcdefab`, // git hash
|
||||
`uuid: 550e8400e29b41d4a716446655440000`, // UUID without dashes
|
||||
`checksum = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"`, // generic checksum
|
||||
}
|
||||
for _, text := range negatives {
|
||||
matches := s.Scan(text)
|
||||
for _, m := range matches {
|
||||
if m.Pattern == "mistral_api_key" {
|
||||
t.Errorf("false positive on: %s", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_NoFalsePositives(t *testing.T) {
|
||||
s := NewScanner(6.0) // high entropy threshold to avoid false positives
|
||||
safe := []string{
|
||||
|
||||
@@ -32,7 +32,8 @@ import (
|
||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||
)
|
||||
|
||||
const version = "v0.1.0-dev"
|
||||
// version is set from Config.Version at init; falls back to "dev".
|
||||
var version = "dev"
|
||||
|
||||
type streamEventMsg struct{ event stream.Event }
|
||||
type turnDoneMsg struct{ err error }
|
||||
@@ -66,6 +67,7 @@ type Config struct {
|
||||
StartWithResumePicker bool // open session picker on launch
|
||||
Skills *skill.Registry // nil = no skills loaded
|
||||
PluginInfos []PluginInfo // discovered plugins for /plugins command
|
||||
Version string // build version string (from ldflags)
|
||||
}
|
||||
|
||||
// PluginInfo is a summary of an installed plugin for TUI display.
|
||||
@@ -118,6 +120,9 @@ type Model struct {
|
||||
}
|
||||
|
||||
func New(sess session.Session, cfg Config) Model {
|
||||
if cfg.Version != "" {
|
||||
version = cfg.Version
|
||||
}
|
||||
ti := textarea.New()
|
||||
ti.Placeholder = "Type a message... (Enter to send, Shift+Enter for newline)"
|
||||
ti.ShowLineNumbers = false
|
||||
|
||||
Reference in New Issue
Block a user