-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.lua
More file actions
171 lines (161 loc) · 7.15 KB
/
Copy pathconfig.lua
File metadata and controls
171 lines (161 loc) · 7.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
-- Native configuration. No provider, secret-store or project-local config reads.
local M = {}
---@type Rose.Config.Defaults
M.defaults = {
legacy = false,
trusted = false,
ollama = {
base_url = "http://127.0.0.1:11434",
model = "qwen2.5-coder:7b",
timeout = 120000,
allow_remote = false,
-- auto prefers vim.net when it can enforce transport safety; see :help rose-http.
transport = "auto",
},
providers = { enabled = false, allow_cloud = false, provider = "ollama" },
speech = {
-- Master gate; cloud speech additionally requires providers.allow_cloud.
enabled = false,
stt = { provider = "auto", model = nil, language = "auto" },
tts = { provider = "auto", model = nil, voice = nil, format = "mp3" },
-- cmd is an argv table; nil detects pw-record/arecord/ffmpeg.
record = { cmd = nil, max_seconds = 60 },
play = { cmd = nil }, -- nil detects pw-play/paplay/aplay/mpv/ffplay
whisper = { url = nil }, -- local whisper.cpp server, loopback http only
piper = { cmd = nil, model = nil }, -- local piper TTS executable
max_audio_bytes = 25 * 1024 * 1024,
max_text_chars = 4096,
},
hub = { python = "python3", max_workers = 4, xet = "auto", high_performance = false },
agent = {
max_iterations = 6,
max_repair_rounds = 1,
max_tool_calls = 8,
max_tool_result = 24000,
max_context = 120000,
},
diver = { lsp = {} },
debug = {},
checks = {},
scip = { path = "index.scip.json" },
flow = { cmd = { "flow", "serve" }, timeout = 600000, bridge = true },
mcp = { servers = {} },
webui = {
enabled = false,
host = "127.0.0.1",
port = 0,
open = true,
max_request_bytes = 16 * 1024 * 1024,
max_clients = 8,
idle_timeout_ms = 30000,
},
}
-- The web UI must never be reachable from another machine, so only these hosts are accepted.
local loopback_hosts = { ["127.0.0.1"] = true, ["::1"] = true, ["localhost"] = true }
local function integer(value, lo, hi, name)
assert(type(name) == "string", "integer(): name must be a string")
assert(lo <= hi, "integer(): lo must not exceed hi")
local range = name .. " must be an integer in " .. lo .. ".." .. hi
assert(type(value) == "number", range)
assert(value % 1 == 0, range)
assert(value >= lo, range)
assert(value <= hi, range)
end
-- Resolve the workspace root to a real, existing directory (absolute, no trailing slash).
local function resolve_workspace(configured)
local uv = vim.uv or vim.loop
local root = configured or uv.cwd()
assert(type(root) == "string", "workspace must be a path")
assert(root ~= "", "workspace must be a path")
root = vim.fn.fnamemodify(root, ":p"):gsub("[/\\]+$", "")
if root == "" then
root = "/"
end
local real = assert(uv.fs_realpath(root), "workspace does not exist")
local stat = uv.fs_stat(real)
assert(stat and stat.type == "directory", "workspace must be a directory")
return real
end
local function validate_webui(webui)
assert(type(webui) == "table", "webui must be a table")
assert(type(webui.enabled) == "boolean", "webui.enabled must be a boolean")
assert(type(webui.open) == "boolean", "webui.open must be a boolean")
assert(loopback_hosts[webui.host] == true, "webui.host must be a loopback address")
integer(webui.port, 0, 65535, "webui.port")
integer(webui.max_request_bytes, 4096, 268435456, "webui.max_request_bytes")
integer(webui.max_clients, 1, 64, "webui.max_clients")
integer(webui.idle_timeout_ms, 100, 600000, "webui.idle_timeout_ms")
end
-- Bound the number of named checks so a pathological config cannot stall startup.
local checks_max = 256
local function validate_checks(checks)
assert(type(checks) == "table", "checks must be a table of named commands")
local count = 0
for name, check in pairs(checks) do
count = count + 1
assert(count <= checks_max, "checks must define at most " .. checks_max .. " entries")
assert(type(name) == "string", "checks must be a table of named configurations")
assert(type(check) == "table", "checks must be a table of named configurations")
if check.filetypes ~= nil then
assert(type(check.filetypes) == "table", "check.filetypes must be an array")
end
end
end
---Merge native setup input and validate core bounds; invalid input raises an error.
---@param opts? Rose.Config
---@return Rose.Config.Resolved
function M.resolve(opts)
opts = opts or {}
assert(type(opts) == "table", "Rose setup options must be a table")
local config = vim.tbl_deep_extend("force", vim.deepcopy(M.defaults), opts)
if opts.agent and opts.agent.max_cycles ~= nil then
integer(opts.agent.max_cycles, 1, 4, "agent.max_cycles")
if opts.agent.max_repair_rounds == nil then
config.agent.max_repair_rounds = opts.agent.max_cycles - 1
end
end
assert(type(config.trusted) == "boolean", "trusted must be a boolean")
config.workspace = resolve_workspace(config.workspace)
integer(config.ollama.timeout, 1, 3600000, "ollama.timeout")
integer(config.agent.max_iterations, 1, 30, "agent.max_iterations")
integer(config.agent.max_repair_rounds, 0, 3, "agent.max_repair_rounds")
config.agent.max_cycles = config.agent.max_repair_rounds + 1
integer(config.agent.max_tool_calls, 1, 32, "agent.max_tool_calls")
integer(config.agent.max_tool_result, 256, 1048576, "agent.max_tool_result")
integer(config.agent.max_context, 1024, 4194304, "agent.max_context")
integer(config.flow.timeout, 1, 3600000, "flow.timeout")
validate_webui(config.webui)
assert(type(config.ollama.model) == "string", "ollama.model is required")
assert(config.ollama.model ~= "", "ollama.model is required")
assert(type(config.ollama.base_url) == "string", "ollama.base_url must be a string")
assert(type(config.providers) == "table", "providers must be a configuration table")
assert(type(config.hub) == "table", "hub must be a configuration table")
assert(type(config.speech) == "table", "speech must be a configuration table")
assert(type(config.speech.enabled) == "boolean", "speech.enabled must be a boolean")
-- Full per-field validation (provider names, format, limits) lives with the module. The
-- module may be absent from a partial install (the web UI tests hide it), so its absence is
-- an operating condition, not a programmer error.
local speech_ok, speech = pcall(require, "rose.speech")
if speech_ok then
speech.config(config)
else
-- Lua leaves a sentinel in package.loaded after a failed load, which would turn every later
-- require into "loop or previous error"; clear it so the web UI can report the real reason.
package.loaded["rose.speech"] = nil
end
validate_checks(config.checks)
assert(type(config.workspace) == "string", "resolved workspace must be a string")
-- The typed Defaults/input merge gains exactly two required fields here:
-- validated real workspace and max_cycles derived from bounded max_repair_rounds.
-- LuaLS does not refine a nominal parent class from those field assignments.
---@cast config Rose.Config.Resolved
return config
end
---Resolve and publish native configuration; invalid input raises an error.
---@param opts? Rose.Config
---@return Rose.Config.Resolved
function M.setup(opts)
M.options = M.resolve(opts)
return M.options
end
return M