Skip to content

Commit 80f7110

Browse files
authored
engine: zero-config local mode on :7654 (HAL-186) (#42)
* engine: zero-config local mode (engine --local / VLE_LOCAL_MODE) on :7654 Adds a local mode that moves the base config to zero-setup defaults BEFORE the file/env layers (which still override): listen on :7654, a localhost Postgres URL matching the bundled/dev database, local file storage, and the Postgres-backed river queue (no Redis needed). The engine then boots with no required configuration — closing the 'database.url is required' gap that otherwise blocks a bare run. - --local flag sets VLE_LOCAL_MODE=true so CLI + Docker (env) share one path. - Adds VLE_STORAGE_LOCAL_ROOT binding for the image's data volume. - cmd/engine is already unauthenticated (single tenant), so local-mode auth needs no extra wiring; documented as dev/local only. - Documented in config.example.yaml; tests cover defaults, truthy forms, env-override precedence, and the non-local missing-DB-URL failure. Foundation for the all-in-one image (HAL-185) + local dashboard (HAL-188). Closes HAL-186. * docs: align documented local Postgres URL with the real default (?sslmode=disable) Per CodeRabbit review on #42 — the example referenced the DSN without the sslmode param that applyLocalDefaults actually injects.
1 parent 6a8badc commit 80f7110

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

cmd/engine/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ func main() {
4646

4747
func run() error {
4848
configPath := flag.String("config", "", "path to config.yaml (optional; env vars take precedence)")
49+
localMode := flag.Bool("local", false, "zero-config local mode: localhost Postgres, local storage, listen on :7654, no setup (sets VLE_LOCAL_MODE)")
4950
flag.Parse()
5051

52+
// --local is sugar for VLE_LOCAL_MODE=true so the CLI flag and the env
53+
// var (used by the all-in-one Docker image) flow through one path in
54+
// config.Load. Set it before Load reads the environment.
55+
if *localMode {
56+
_ = os.Setenv("VLE_LOCAL_MODE", "true")
57+
}
58+
5159
cfg, err := config.Load(*configPath)
5260
if err != nil {
5361
return fmt.Errorf("load config: %w", err)
@@ -56,6 +64,7 @@ func run() error {
5664
logger := newLogger(cfg.Log)
5765
logger.Info("starting vectorless-engine",
5866
"version", version,
67+
"local_mode", config.LocalModeEnabled(),
5968
"storage_driver", cfg.Storage.Driver,
6069
"queue_driver", cfg.Queue.Driver,
6170
"llm_driver", cfg.LLM.Driver,

config.example.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@
1313
#
1414
# `vectorless-engine config print` prints the effective config with secrets
1515
# redacted; `vectorless-engine config check` validates it and exits 0/1.
16+
#
17+
# ZERO-CONFIG LOCAL MODE
18+
# ----------------------
19+
# Run `engine --local` (or set VLE_LOCAL_MODE=true) to boot with no config
20+
# at all: it listens on :7654, points at a localhost Postgres
21+
# (postgres://vectorless:vectorless@localhost:5432/vectorless?sslmode=disable), uses local
22+
# file storage and the Postgres-backed river queue, and requires no API key
23+
# to call the engine. This matches the all-in-one Docker image, where
24+
# Postgres is bundled in the same container. You still supply an LLM
25+
# provider key (e.g. VLE_LLM_ANTHROPIC_API_KEY) for ingestion + retrieval.
26+
# Override any local default with the usual env/flags, e.g.
27+
# VLE_SERVER_ADDR=:9000 or VLE_STORAGE_LOCAL_ROOT=/data/documents.
28+
# Local mode is for dev/local use — do NOT expose it to the public internet.
1629

1730
server:
1831
addr: ":8080"

pkg/config/config.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,10 +790,64 @@ func Default() Config {
790790
}
791791
}
792792

793+
// localDefaultAddr is the canonical local-mode listen address. The whole
794+
// product (engine API + bundled dashboard) is reachable at
795+
// localhost:7654 so every doc, link, and quickstart can say one number.
796+
const localDefaultAddr = ":7654"
797+
798+
// defaultLocalDatabaseURL is the Postgres DSN local mode assumes when no
799+
// URL is configured. It matches the bundled Postgres in the all-in-one
800+
// Docker image and the dev docker-compose (user/pass/db all "vectorless"
801+
// on localhost:5432), so a bare `engine --local` next to those services
802+
// just connects.
803+
const defaultLocalDatabaseURL = "postgres://vectorless:vectorless@localhost:5432/vectorless?sslmode=disable"
804+
805+
// LocalModeEnabled reports whether zero-config local mode was requested
806+
// via the VLE_LOCAL_MODE env var (truthy: 1/true/yes/on). The engine's
807+
// --local flag sets this var before Load runs, so the CLI flag and the
808+
// env var (used by the Docker image) share one code path.
809+
func LocalModeEnabled() bool {
810+
switch strings.ToLower(strings.TrimSpace(os.Getenv("VLE_LOCAL_MODE"))) {
811+
case "1", "true", "yes", "on":
812+
return true
813+
}
814+
return false
815+
}
816+
817+
// applyLocalDefaults rewrites the base config for zero-config local
818+
// running: the canonical :7654 port, a localhost Postgres URL matching
819+
// the bundled/dev database, local file storage, and the Postgres-backed
820+
// river queue (no Redis required). It runs on the Default() base BEFORE
821+
// the YAML file and env overrides are applied, so any value the operator
822+
// sets explicitly still wins — local mode only moves the starting point
823+
// so the engine boots with no required configuration.
824+
//
825+
// Auth: the standalone engine (cmd/engine) is already unauthenticated —
826+
// it serves a single logical tenant with no API key — so "local-mode
827+
// auth" needs no extra wiring here. This is dev/local only and must not
828+
// be exposed to the public internet.
829+
func applyLocalDefaults(c *Config) {
830+
c.Server.Addr = localDefaultAddr
831+
c.Database.URL = defaultLocalDatabaseURL
832+
c.Storage.Driver = "local"
833+
if c.Storage.Local.Root == "" {
834+
c.Storage.Local.Root = "./data/documents"
835+
}
836+
c.Queue.Driver = "river"
837+
}
838+
793839
// Load reads configuration from a YAML file (optional) and applies
794840
// environment overrides on top. Pass an empty path to skip the file.
841+
//
842+
// When VLE_LOCAL_MODE is truthy (or the engine is run with --local, which
843+
// sets it), zero-config local defaults are applied to the base before the
844+
// file/env layers, so the engine boots on :7654 against a localhost
845+
// Postgres with no required configuration. File and env still override.
795846
func Load(path string) (Config, error) {
796847
cfg := Default()
848+
if LocalModeEnabled() {
849+
applyLocalDefaults(&cfg)
850+
}
797851
if path != "" {
798852
data, err := os.ReadFile(path)
799853
if err != nil {
@@ -869,6 +923,9 @@ func applyEnvOverrides(c *Config) {
869923
if v := os.Getenv("VLE_STORAGE_DRIVER"); v != "" {
870924
c.Storage.Driver = v
871925
}
926+
if v := os.Getenv("VLE_STORAGE_LOCAL_ROOT"); v != "" {
927+
c.Storage.Local.Root = v
928+
}
872929
if v := os.Getenv("VLE_QUEUE_DRIVER"); v != "" {
873930
c.Queue.Driver = v
874931
}

pkg/config/config_local_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package config
2+
3+
import "testing"
4+
5+
// TestLocalModeDefaults: with VLE_LOCAL_MODE set, Load with no file and no
6+
// other env boots a complete, valid config — :7654, a localhost Postgres
7+
// URL, local storage, river queue — with nothing else required.
8+
func TestLocalModeDefaults(t *testing.T) {
9+
t.Setenv("VLE_LOCAL_MODE", "true")
10+
11+
cfg, err := Load("")
12+
if err != nil {
13+
t.Fatalf("local-mode Load() with no other config should succeed, got: %v", err)
14+
}
15+
if cfg.Server.Addr != ":7654" {
16+
t.Errorf("local mode server.addr = %q, want :7654", cfg.Server.Addr)
17+
}
18+
if cfg.Database.URL != defaultLocalDatabaseURL {
19+
t.Errorf("local mode database.url = %q, want %q", cfg.Database.URL, defaultLocalDatabaseURL)
20+
}
21+
if cfg.Storage.Driver != "local" {
22+
t.Errorf("local mode storage.driver = %q, want local", cfg.Storage.Driver)
23+
}
24+
if cfg.Queue.Driver != "river" {
25+
t.Errorf("local mode queue.driver = %q, want river", cfg.Queue.Driver)
26+
}
27+
if cfg.Storage.Local.Root == "" {
28+
t.Error("local mode storage.local.root must be set")
29+
}
30+
}
31+
32+
// TestLocalModeTruthyForms: the env flag accepts the usual truthy spellings
33+
// and ignores everything else.
34+
func TestLocalModeTruthyForms(t *testing.T) {
35+
for _, v := range []string{"1", "true", "TRUE", "yes", "on"} {
36+
t.Setenv("VLE_LOCAL_MODE", v)
37+
if !LocalModeEnabled() {
38+
t.Errorf("VLE_LOCAL_MODE=%q should enable local mode", v)
39+
}
40+
}
41+
for _, v := range []string{"", "0", "false", "no", "off", "nope"} {
42+
t.Setenv("VLE_LOCAL_MODE", v)
43+
if LocalModeEnabled() {
44+
t.Errorf("VLE_LOCAL_MODE=%q should NOT enable local mode", v)
45+
}
46+
}
47+
}
48+
49+
// TestLocalModeEnvOverridesWin: local mode only moves the starting point —
50+
// explicit env values still override the local defaults.
51+
func TestLocalModeEnvOverridesWin(t *testing.T) {
52+
t.Setenv("VLE_LOCAL_MODE", "true")
53+
t.Setenv("VLE_SERVER_ADDR", ":9999")
54+
t.Setenv("VLE_DATABASE_URL", "postgres://custom:custom@db:5432/custom?sslmode=disable")
55+
t.Setenv("VLE_STORAGE_LOCAL_ROOT", "/srv/docs")
56+
57+
cfg, err := Load("")
58+
if err != nil {
59+
t.Fatalf("Load() failed: %v", err)
60+
}
61+
if cfg.Server.Addr != ":9999" {
62+
t.Errorf("env should override local addr: got %q, want :9999", cfg.Server.Addr)
63+
}
64+
if cfg.Database.URL != "postgres://custom:custom@db:5432/custom?sslmode=disable" {
65+
t.Errorf("env should override local db url, got %q", cfg.Database.URL)
66+
}
67+
if cfg.Storage.Local.Root != "/srv/docs" {
68+
t.Errorf("VLE_STORAGE_LOCAL_ROOT should set storage root, got %q", cfg.Storage.Local.Root)
69+
}
70+
}
71+
72+
// TestNonLocalModeUnchanged: without the flag the historical defaults hold
73+
// (:8080), and the engine still requires a database URL for the river
74+
// queue — i.e. local mode is the ONLY thing that injects one.
75+
func TestNonLocalModeUnchanged(t *testing.T) {
76+
t.Setenv("VLE_LOCAL_MODE", "")
77+
// Provide a DB URL so validation passes for the river default.
78+
t.Setenv("VLE_DATABASE_URL", "postgres://x:x@localhost:5432/x?sslmode=disable")
79+
80+
cfg, err := Load("")
81+
if err != nil {
82+
t.Fatalf("Load() failed: %v", err)
83+
}
84+
if cfg.Server.Addr != ":8080" {
85+
t.Errorf("non-local addr = %q, want :8080", cfg.Server.Addr)
86+
}
87+
}
88+
89+
// TestNonLocalModeMissingDBURLFails proves the local-mode injection is what
90+
// removes the "no required config" gap: without it and without a DB URL,
91+
// the river queue fails validation.
92+
func TestNonLocalModeMissingDBURLFails(t *testing.T) {
93+
t.Setenv("VLE_LOCAL_MODE", "")
94+
t.Setenv("VLE_DATABASE_URL", "")
95+
96+
if _, err := Load(""); err == nil {
97+
t.Fatal("expected validation error for river queue with no database.url, got nil")
98+
}
99+
}

0 commit comments

Comments
 (0)