@poolzin/pool-bot 2026.3.22 → 2026.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,402 @@
1
+ import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
2
+ import { OLLAMA_DEFAULT_BASE_URL, buildOllamaModelDefinition, enrichOllamaModelsWithContext, fetchOllamaModels, resolveOllamaApiBase, } from "../agents/ollama-models.js";
3
+ import { WizardCancelledError } from "../wizard/prompts.js";
4
+ import { isRemoteEnvironment } from "./oauth-env.js";
5
+ import { applyAgentDefaultModelPrimary } from "./onboard-auth.config-shared.js";
6
+ import { openUrl } from "./onboard-helpers.js";
7
+ export { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-models.js";
8
+ export const OLLAMA_DEFAULT_MODEL = "glm-4.7-flash";
9
+ const OLLAMA_SUGGESTED_MODELS_LOCAL = ["glm-4.7-flash"];
10
+ const OLLAMA_SUGGESTED_MODELS_CLOUD = ["kimi-k2.5:cloud", "minimax-m2.5:cloud", "glm-5:cloud"];
11
+ function normalizeOllamaModelName(value) {
12
+ const trimmed = value?.trim();
13
+ if (!trimmed) {
14
+ return undefined;
15
+ }
16
+ if (trimmed.toLowerCase().startsWith("ollama/")) {
17
+ const withoutPrefix = trimmed.slice("ollama/".length).trim();
18
+ return withoutPrefix || undefined;
19
+ }
20
+ return trimmed;
21
+ }
22
+ function isOllamaCloudModel(modelName) {
23
+ return Boolean(modelName?.trim().toLowerCase().endsWith(":cloud"));
24
+ }
25
+ function formatOllamaPullStatus(status) {
26
+ const trimmed = status.trim();
27
+ const partStatusMatch = trimmed.match(/^([a-z-]+)\s+(?:sha256:)?[a-f0-9]{8,}$/i);
28
+ if (partStatusMatch) {
29
+ return { text: `${partStatusMatch[1]} part`, hidePercent: false };
30
+ }
31
+ if (/^verifying\b.*\bdigest\b/i.test(trimmed)) {
32
+ return { text: "verifying digest", hidePercent: true };
33
+ }
34
+ return { text: trimmed, hidePercent: false };
35
+ }
36
+ /** Check if the user is signed in to Ollama cloud via /api/me. */
37
+ async function checkOllamaCloudAuth(baseUrl) {
38
+ try {
39
+ const apiBase = resolveOllamaApiBase(baseUrl);
40
+ const response = await fetch(`${apiBase}/api/me`, {
41
+ method: "POST",
42
+ signal: AbortSignal.timeout(5000),
43
+ });
44
+ if (response.status === 401) {
45
+ // 401 body contains { error, signin_url }
46
+ const data = (await response.json());
47
+ return { signedIn: false, signinUrl: data.signin_url };
48
+ }
49
+ if (!response.ok) {
50
+ return { signedIn: false };
51
+ }
52
+ return { signedIn: true };
53
+ }
54
+ catch {
55
+ // /api/me not supported or unreachable — fail closed so cloud mode
56
+ // doesn't silently skip auth; the caller handles the fallback.
57
+ return { signedIn: false };
58
+ }
59
+ }
60
+ async function pullOllamaModelCore(params) {
61
+ const { onStatus } = params;
62
+ const baseUrl = resolveOllamaApiBase(params.baseUrl);
63
+ const modelName = normalizeOllamaModelName(params.modelName) ?? params.modelName.trim();
64
+ try {
65
+ const response = await fetch(`${baseUrl}/api/pull`, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ name: modelName }),
69
+ });
70
+ if (!response.ok) {
71
+ return {
72
+ ok: false,
73
+ kind: "http",
74
+ message: `Failed to download ${modelName} (HTTP ${response.status})`,
75
+ };
76
+ }
77
+ if (!response.body) {
78
+ return {
79
+ ok: false,
80
+ kind: "no-body",
81
+ message: `Failed to download ${modelName} (no response body)`,
82
+ };
83
+ }
84
+ const reader = response.body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let buffer = "";
87
+ const layers = new Map();
88
+ const parseLine = (line) => {
89
+ const trimmed = line.trim();
90
+ if (!trimmed) {
91
+ return { ok: true };
92
+ }
93
+ try {
94
+ const chunk = JSON.parse(trimmed);
95
+ if (chunk.error) {
96
+ return {
97
+ ok: false,
98
+ kind: "chunk-error",
99
+ message: `Download failed: ${chunk.error}`,
100
+ };
101
+ }
102
+ if (!chunk.status) {
103
+ return { ok: true };
104
+ }
105
+ if (chunk.total && chunk.completed !== undefined) {
106
+ layers.set(chunk.status, { total: chunk.total, completed: chunk.completed });
107
+ let totalSum = 0;
108
+ let completedSum = 0;
109
+ for (const layer of layers.values()) {
110
+ totalSum += layer.total;
111
+ completedSum += layer.completed;
112
+ }
113
+ const percent = totalSum > 0 ? Math.round((completedSum / totalSum) * 100) : null;
114
+ onStatus?.(chunk.status, percent);
115
+ }
116
+ else {
117
+ onStatus?.(chunk.status, null);
118
+ }
119
+ }
120
+ catch {
121
+ // Ignore malformed lines from streaming output.
122
+ }
123
+ return { ok: true };
124
+ };
125
+ for (;;) {
126
+ const { done, value } = await reader.read();
127
+ if (done) {
128
+ break;
129
+ }
130
+ buffer += decoder.decode(value, { stream: true });
131
+ const lines = buffer.split("\n");
132
+ buffer = lines.pop() ?? "";
133
+ for (const line of lines) {
134
+ const parsed = parseLine(line);
135
+ if (!parsed.ok) {
136
+ return parsed;
137
+ }
138
+ }
139
+ }
140
+ const trailing = buffer.trim();
141
+ if (trailing) {
142
+ const parsed = parseLine(trailing);
143
+ if (!parsed.ok) {
144
+ return parsed;
145
+ }
146
+ }
147
+ return { ok: true };
148
+ }
149
+ catch (err) {
150
+ const reason = err instanceof Error ? err.message : String(err);
151
+ return {
152
+ ok: false,
153
+ kind: "network",
154
+ message: `Failed to download ${modelName}: ${reason}`,
155
+ };
156
+ }
157
+ }
158
+ /** Pull a model from Ollama, streaming progress updates. */
159
+ async function pullOllamaModel(baseUrl, modelName, prompter) {
160
+ const spinner = prompter.progress(`Downloading ${modelName}...`);
161
+ const result = await pullOllamaModelCore({
162
+ baseUrl,
163
+ modelName,
164
+ onStatus: (status, percent) => {
165
+ const displayStatus = formatOllamaPullStatus(status);
166
+ if (displayStatus.hidePercent) {
167
+ spinner.update(`Downloading ${modelName} - ${displayStatus.text}`);
168
+ }
169
+ else {
170
+ spinner.update(`Downloading ${modelName} - ${displayStatus.text} - ${percent ?? 0}%`);
171
+ }
172
+ },
173
+ });
174
+ if (!result.ok) {
175
+ spinner.stop(result.message);
176
+ return false;
177
+ }
178
+ spinner.stop(`Downloaded ${modelName}`);
179
+ return true;
180
+ }
181
+ async function pullOllamaModelNonInteractive(baseUrl, modelName, runtime) {
182
+ runtime.log(`Downloading ${modelName}...`);
183
+ const result = await pullOllamaModelCore({ baseUrl, modelName });
184
+ if (!result.ok) {
185
+ runtime.error(result.message);
186
+ return false;
187
+ }
188
+ runtime.log(`Downloaded ${modelName}`);
189
+ return true;
190
+ }
191
+ function buildOllamaModelsConfig(modelNames, discoveredModelsByName) {
192
+ return modelNames.map((name) => buildOllamaModelDefinition(name, discoveredModelsByName?.get(name)?.contextWindow));
193
+ }
194
+ function applyOllamaProviderConfig(cfg, baseUrl, modelNames, discoveredModelsByName) {
195
+ return {
196
+ ...cfg,
197
+ models: {
198
+ ...cfg.models,
199
+ mode: cfg.models?.mode ?? "merge",
200
+ providers: {
201
+ ...cfg.models?.providers,
202
+ ollama: {
203
+ baseUrl,
204
+ api: "ollama",
205
+ apiKey: "OLLAMA_API_KEY", // pragma: allowlist secret
206
+ models: buildOllamaModelsConfig(modelNames, discoveredModelsByName),
207
+ },
208
+ },
209
+ },
210
+ };
211
+ }
212
+ async function storeOllamaCredential(agentDir) {
213
+ await upsertAuthProfileWithLock({
214
+ profileId: "ollama:default",
215
+ credential: { type: "api_key", provider: "ollama", key: "ollama-local" },
216
+ agentDir,
217
+ });
218
+ }
219
+ /**
220
+ * Interactive: prompt for base URL, discover models, configure provider.
221
+ * Model selection is handled by the standard model picker downstream.
222
+ */
223
+ export async function promptAndConfigureOllama(params) {
224
+ const { prompter } = params;
225
+ // 1. Prompt base URL
226
+ const baseUrlRaw = await prompter.text({
227
+ message: "Ollama base URL",
228
+ initialValue: OLLAMA_DEFAULT_BASE_URL,
229
+ placeholder: OLLAMA_DEFAULT_BASE_URL,
230
+ validate: (value) => (value?.trim() ? undefined : "Required"),
231
+ });
232
+ const configuredBaseUrl = String(baseUrlRaw ?? "")
233
+ .trim()
234
+ .replace(/\/+$/, "");
235
+ const baseUrl = resolveOllamaApiBase(configuredBaseUrl);
236
+ // 2. Check reachability
237
+ const { reachable, models } = await fetchOllamaModels(baseUrl);
238
+ if (!reachable) {
239
+ await prompter.note([
240
+ `Ollama could not be reached at ${baseUrl}.`,
241
+ "Download it at https://ollama.com/download",
242
+ "",
243
+ "Start Ollama and re-run onboarding.",
244
+ ].join("\n"), "Ollama");
245
+ throw new WizardCancelledError("Ollama not reachable");
246
+ }
247
+ const enrichedModels = await enrichOllamaModelsWithContext(baseUrl, models.slice(0, 50));
248
+ const discoveredModelsByName = new Map(enrichedModels.map((model) => [model.name, model]));
249
+ const modelNames = models.map((m) => m.name);
250
+ // 3. Mode selection
251
+ const mode = (await prompter.select({
252
+ message: "Ollama mode",
253
+ options: [
254
+ { value: "remote", label: "Cloud + Local", hint: "Ollama cloud models + local models" },
255
+ { value: "local", label: "Local", hint: "Local models only" },
256
+ ],
257
+ }));
258
+ // 4. Cloud auth — check /api/me upfront for remote (cloud+local) mode
259
+ let cloudAuthVerified = false;
260
+ if (mode === "remote") {
261
+ const authResult = await checkOllamaCloudAuth(baseUrl);
262
+ if (!authResult.signedIn) {
263
+ if (authResult.signinUrl) {
264
+ if (!isRemoteEnvironment()) {
265
+ await openUrl(authResult.signinUrl);
266
+ }
267
+ await prompter.note(["Sign in to Ollama Cloud:", authResult.signinUrl].join("\n"), "Ollama Cloud");
268
+ const confirmed = await prompter.confirm({
269
+ message: "Have you signed in?",
270
+ });
271
+ if (!confirmed) {
272
+ throw new WizardCancelledError("Ollama cloud sign-in cancelled");
273
+ }
274
+ // Re-check after user claims sign-in
275
+ const recheck = await checkOllamaCloudAuth(baseUrl);
276
+ if (!recheck.signedIn) {
277
+ throw new WizardCancelledError("Ollama cloud sign-in required");
278
+ }
279
+ cloudAuthVerified = true;
280
+ }
281
+ else {
282
+ // No signin URL available (older server, unreachable /api/me, or custom gateway).
283
+ await prompter.note([
284
+ "Could not verify Ollama Cloud authentication.",
285
+ "Cloud models may not work until you sign in at https://ollama.com.",
286
+ ].join("\n"), "Ollama Cloud");
287
+ const continueAnyway = await prompter.confirm({
288
+ message: "Continue without cloud auth?",
289
+ });
290
+ if (!continueAnyway) {
291
+ throw new WizardCancelledError("Ollama cloud auth could not be verified");
292
+ }
293
+ // Cloud auth unverified — fall back to local defaults so the model
294
+ // picker doesn't steer toward cloud models that may fail.
295
+ }
296
+ }
297
+ else {
298
+ cloudAuthVerified = true;
299
+ }
300
+ }
301
+ // 5. Model ordering — suggested models first.
302
+ // Use cloud defaults only when auth was actually verified; otherwise fall
303
+ // back to local defaults so the user isn't steered toward cloud models
304
+ // that may fail at runtime.
305
+ const suggestedModels = mode === "local" || !cloudAuthVerified
306
+ ? OLLAMA_SUGGESTED_MODELS_LOCAL
307
+ : OLLAMA_SUGGESTED_MODELS_CLOUD;
308
+ const orderedModelNames = [
309
+ ...suggestedModels,
310
+ ...modelNames.filter((name) => !suggestedModels.includes(name)),
311
+ ];
312
+ const defaultModelId = suggestedModels[0] ?? OLLAMA_DEFAULT_MODEL;
313
+ const config = applyOllamaProviderConfig(params.cfg, baseUrl, orderedModelNames, discoveredModelsByName);
314
+ return { config, defaultModelId };
315
+ }
316
+ /** Non-interactive: auto-discover models and configure provider. */
317
+ export async function configureOllamaNonInteractive(params) {
318
+ const { opts, runtime } = params;
319
+ const configuredBaseUrl = (opts.customBaseUrl?.trim() || OLLAMA_DEFAULT_BASE_URL).replace(/\/+$/, "");
320
+ const baseUrl = resolveOllamaApiBase(configuredBaseUrl);
321
+ const { reachable, models } = await fetchOllamaModels(baseUrl);
322
+ const explicitModel = normalizeOllamaModelName(opts.customModelId);
323
+ if (!reachable) {
324
+ runtime.error([
325
+ `Ollama could not be reached at ${baseUrl}.`,
326
+ "Download it at https://ollama.com/download",
327
+ ].join("\n"));
328
+ runtime.exit(1);
329
+ return params.nextConfig;
330
+ }
331
+ await storeOllamaCredential();
332
+ const enrichedModels = await enrichOllamaModelsWithContext(baseUrl, models.slice(0, 50));
333
+ const discoveredModelsByName = new Map(enrichedModels.map((model) => [model.name, model]));
334
+ const modelNames = models.map((m) => m.name);
335
+ // Apply local suggested model ordering.
336
+ const suggestedModels = OLLAMA_SUGGESTED_MODELS_LOCAL;
337
+ const orderedModelNames = [
338
+ ...suggestedModels,
339
+ ...modelNames.filter((name) => !suggestedModels.includes(name)),
340
+ ];
341
+ const requestedDefaultModelId = explicitModel ?? suggestedModels[0];
342
+ let pulledRequestedModel = false;
343
+ const availableModelNames = new Set(modelNames);
344
+ const requestedCloudModel = isOllamaCloudModel(requestedDefaultModelId);
345
+ if (requestedCloudModel) {
346
+ availableModelNames.add(requestedDefaultModelId);
347
+ }
348
+ // Pull if model not in discovered list and Ollama is reachable
349
+ if (!requestedCloudModel && !modelNames.includes(requestedDefaultModelId)) {
350
+ pulledRequestedModel = await pullOllamaModelNonInteractive(baseUrl, requestedDefaultModelId, runtime);
351
+ if (pulledRequestedModel) {
352
+ availableModelNames.add(requestedDefaultModelId);
353
+ }
354
+ }
355
+ let allModelNames = orderedModelNames;
356
+ let defaultModelId = requestedDefaultModelId;
357
+ if ((pulledRequestedModel || requestedCloudModel) &&
358
+ !allModelNames.includes(requestedDefaultModelId)) {
359
+ allModelNames = [...allModelNames, requestedDefaultModelId];
360
+ }
361
+ if (!availableModelNames.has(requestedDefaultModelId)) {
362
+ if (availableModelNames.size > 0) {
363
+ const firstAvailableModel = allModelNames.find((name) => availableModelNames.has(name)) ??
364
+ Array.from(availableModelNames)[0];
365
+ defaultModelId = firstAvailableModel;
366
+ runtime.log(`Ollama model ${requestedDefaultModelId} was not available; using ${defaultModelId} instead.`);
367
+ }
368
+ else {
369
+ runtime.error([
370
+ `No Ollama models are available at ${baseUrl}.`,
371
+ "Pull a model first, then re-run onboarding.",
372
+ ].join("\n"));
373
+ runtime.exit(1);
374
+ return params.nextConfig;
375
+ }
376
+ }
377
+ const config = applyOllamaProviderConfig(params.nextConfig, baseUrl, allModelNames, discoveredModelsByName);
378
+ const modelRef = `ollama/${defaultModelId}`;
379
+ runtime.log(`Default Ollama model: ${defaultModelId}`);
380
+ return applyAgentDefaultModelPrimary(config, modelRef);
381
+ }
382
+ /** Pull the configured default Ollama model if it isn't already available locally. */
383
+ export async function ensureOllamaModelPulled(params) {
384
+ const modelCfg = params.config.agents?.defaults?.model;
385
+ const modelId = typeof modelCfg === "string" ? modelCfg : modelCfg?.primary;
386
+ if (!modelId?.startsWith("ollama/")) {
387
+ return;
388
+ }
389
+ const baseUrl = params.config.models?.providers?.ollama?.baseUrl ?? OLLAMA_DEFAULT_BASE_URL;
390
+ const modelName = modelId.slice("ollama/".length);
391
+ if (isOllamaCloudModel(modelName)) {
392
+ return;
393
+ }
394
+ const { models } = await fetchOllamaModels(baseUrl);
395
+ if (models.some((m) => m.name === modelName)) {
396
+ return;
397
+ }
398
+ const pulled = await pullOllamaModel(baseUrl, modelName, params.prompter);
399
+ if (!pulled) {
400
+ throw new WizardCancelledError("Failed to download selected Ollama model");
401
+ }
402
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Commands Security - Owner-Only Enforcement
3
+ *
4
+ * Security hardening for sensitive commands:
5
+ * - /config - Configuration management
6
+ * - /debug - Runtime debug information
7
+ *
8
+ * GHSA-commands-owner-only: Require sender ownership for owner-only commands
9
+ */
10
+ /**
11
+ * List of owner-only commands
12
+ */
13
+ export const OWNER_ONLY_COMMANDS = new Set([
14
+ "config",
15
+ "debug",
16
+ "config.set",
17
+ "config.get",
18
+ "config.reset",
19
+ "debug.gateway",
20
+ "debug.sessions",
21
+ "debug.memory",
22
+ ]);
23
+ /**
24
+ * Check if a command requires owner-only access
25
+ */
26
+ export function isOwnerOnlyCommand(command) {
27
+ const normalized = command.toLowerCase().trim();
28
+ return OWNER_ONLY_COMMANDS.has(normalized);
29
+ }
30
+ /**
31
+ * Validate sender ownership for owner-only commands
32
+ *
33
+ * Returns true if sender is authorized, false otherwise
34
+ */
35
+ export function validateOwnerOnlyAccess(params) {
36
+ const { command, senderId, config } = params;
37
+ // Check if command requires owner-only access
38
+ if (!isOwnerOnlyCommand(command)) {
39
+ return true;
40
+ }
41
+ // Get owner ID from config (check multiple possible locations)
42
+ const ownerId = config.gateway?.ownerId ||
43
+ config?.ownerId ||
44
+ config.auth?.ownerId;
45
+ if (!ownerId) {
46
+ // No owner configured, allow access (legacy mode)
47
+ return true;
48
+ }
49
+ // Validate sender ownership
50
+ return senderId === ownerId;
51
+ }
52
+ /**
53
+ * Get owner-only command error message
54
+ */
55
+ export function getOwnerOnlyErrorMessage(command) {
56
+ return `Command "${command}" requires owner access. Only the gateway owner can execute this command.`;
57
+ }
58
+ /**
59
+ * Analyze command security requirements
60
+ */
61
+ export function analyzeCommandSecurity(params) {
62
+ const { command, senderId, config } = params;
63
+ const isOwnerOnly = isOwnerOnlyCommand(command);
64
+ const senderAuthorized = validateOwnerOnlyAccess({ command, senderId, config });
65
+ // Determine security level
66
+ let securityLevel;
67
+ if (isOwnerOnly) {
68
+ securityLevel = "owner";
69
+ }
70
+ else if (command.startsWith("admin.")) {
71
+ securityLevel = "admin";
72
+ }
73
+ else if (command.startsWith("user.")) {
74
+ securityLevel = "user";
75
+ }
76
+ else {
77
+ securityLevel = "public";
78
+ }
79
+ return {
80
+ command,
81
+ isOwnerOnly,
82
+ senderAuthorized,
83
+ requiresElevation: isOwnerOnly && !senderAuthorized,
84
+ securityLevel,
85
+ };
86
+ }
@@ -0,0 +1,207 @@
1
+ import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
2
+ import { applyAuthProfileConfig } from "./onboard-auth.js";
3
+ export const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128000;
4
+ export const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
5
+ export const SELF_HOSTED_DEFAULT_COST = {
6
+ input: 0,
7
+ output: 0,
8
+ cacheRead: 0,
9
+ cacheWrite: 0,
10
+ };
11
+ export function applyProviderDefaultModel(cfg, modelRef) {
12
+ const existingModel = cfg.agents?.defaults?.model;
13
+ const fallbacks = existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
14
+ ? existingModel.fallbacks
15
+ : undefined;
16
+ return {
17
+ ...cfg,
18
+ agents: {
19
+ ...cfg.agents,
20
+ defaults: {
21
+ ...cfg.agents?.defaults,
22
+ model: {
23
+ ...(fallbacks ? { fallbacks } : undefined),
24
+ primary: modelRef,
25
+ },
26
+ },
27
+ },
28
+ };
29
+ }
30
+ function buildOpenAICompatibleSelfHostedProviderConfig(params) {
31
+ const modelRef = `${params.providerId}/${params.modelId}`;
32
+ const profileId = `${params.providerId}:default`;
33
+ return {
34
+ config: {
35
+ ...params.cfg,
36
+ models: {
37
+ ...params.cfg.models,
38
+ mode: params.cfg.models?.mode ?? "merge",
39
+ providers: {
40
+ ...params.cfg.models?.providers,
41
+ [params.providerId]: {
42
+ baseUrl: params.baseUrl,
43
+ api: "openai-completions",
44
+ apiKey: params.providerApiKey,
45
+ models: [
46
+ {
47
+ id: params.modelId,
48
+ name: params.modelId,
49
+ reasoning: params.reasoning ?? false,
50
+ input: params.input ?? ["text"],
51
+ cost: SELF_HOSTED_DEFAULT_COST,
52
+ contextWindow: params.contextWindow ?? SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
53
+ maxTokens: params.maxTokens ?? SELF_HOSTED_DEFAULT_MAX_TOKENS,
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ },
59
+ },
60
+ modelId: params.modelId,
61
+ modelRef,
62
+ profileId,
63
+ };
64
+ }
65
+ function buildSelfHostedProviderAuthResult(result) {
66
+ return {
67
+ profiles: [
68
+ {
69
+ profileId: result.profileId,
70
+ credential: result.credential,
71
+ },
72
+ ],
73
+ configPatch: result.config,
74
+ defaultModel: result.modelRef,
75
+ };
76
+ }
77
+ export async function promptAndConfigureOpenAICompatibleSelfHostedProvider(params) {
78
+ const baseUrlRaw = await params.prompter.text({
79
+ message: `${params.providerLabel} base URL`,
80
+ initialValue: params.defaultBaseUrl,
81
+ placeholder: params.defaultBaseUrl,
82
+ validate: (value) => (value?.trim() ? undefined : "Required"),
83
+ });
84
+ const apiKeyRaw = await params.prompter.text({
85
+ message: `${params.providerLabel} API key`,
86
+ placeholder: "sk-... (or any non-empty string)",
87
+ validate: (value) => (value?.trim() ? undefined : "Required"),
88
+ });
89
+ const modelIdRaw = await params.prompter.text({
90
+ message: `${params.providerLabel} model`,
91
+ placeholder: params.modelPlaceholder,
92
+ validate: (value) => (value?.trim() ? undefined : "Required"),
93
+ });
94
+ const baseUrl = String(baseUrlRaw ?? "")
95
+ .trim()
96
+ .replace(/\/+$/, "");
97
+ const apiKey = String(apiKeyRaw ?? "").trim();
98
+ const modelId = String(modelIdRaw ?? "").trim();
99
+ const credential = {
100
+ type: "api_key",
101
+ provider: params.providerId,
102
+ key: apiKey,
103
+ };
104
+ const configured = buildOpenAICompatibleSelfHostedProviderConfig({
105
+ cfg: params.cfg,
106
+ providerId: params.providerId,
107
+ baseUrl,
108
+ providerApiKey: params.defaultApiKeyEnvVar,
109
+ modelId,
110
+ input: params.input,
111
+ reasoning: params.reasoning,
112
+ contextWindow: params.contextWindow,
113
+ maxTokens: params.maxTokens,
114
+ });
115
+ return {
116
+ config: configured.config,
117
+ credential,
118
+ modelId: configured.modelId,
119
+ modelRef: configured.modelRef,
120
+ profileId: configured.profileId,
121
+ };
122
+ }
123
+ export async function promptAndConfigureOpenAICompatibleSelfHostedProviderAuth(params) {
124
+ const result = await promptAndConfigureOpenAICompatibleSelfHostedProvider(params);
125
+ return buildSelfHostedProviderAuthResult(result);
126
+ }
127
+ export async function discoverOpenAICompatibleSelfHostedProvider(params) {
128
+ if (params.ctx.config.models?.providers?.[params.providerId]) {
129
+ return null;
130
+ }
131
+ const { apiKey, discoveryApiKey } = params.ctx.resolveProviderApiKey(params.providerId);
132
+ if (!apiKey) {
133
+ return null;
134
+ }
135
+ return {
136
+ provider: {
137
+ ...(await params.buildProvider({ apiKey: discoveryApiKey })),
138
+ apiKey,
139
+ },
140
+ };
141
+ }
142
+ function buildMissingNonInteractiveModelIdMessage(params) {
143
+ return [
144
+ `Missing --custom-model-id for --auth-choice ${params.authChoice}.`,
145
+ `Pass the ${params.providerLabel} model id to use, for example ${params.modelPlaceholder}.`,
146
+ ].join("\n");
147
+ }
148
+ function buildSelfHostedProviderCredential(params) {
149
+ return params.ctx.toApiKeyCredential({
150
+ provider: params.providerId,
151
+ resolved: params.resolved,
152
+ });
153
+ }
154
+ export async function configureOpenAICompatibleSelfHostedProviderNonInteractive(params) {
155
+ const baseUrl = (params.ctx.opts.customBaseUrl?.trim() || params.defaultBaseUrl).replace(/\/+$/, "");
156
+ const modelId = params.ctx.opts.customModelId?.trim();
157
+ if (!modelId) {
158
+ params.ctx.runtime.error(buildMissingNonInteractiveModelIdMessage({
159
+ authChoice: params.ctx.authChoice,
160
+ providerLabel: params.providerLabel,
161
+ modelPlaceholder: params.modelPlaceholder,
162
+ }));
163
+ params.ctx.runtime.exit(1);
164
+ return null;
165
+ }
166
+ const resolved = await params.ctx.resolveApiKey({
167
+ provider: params.providerId,
168
+ flagValue: params.ctx.opts.customApiKey,
169
+ flagName: "--custom-api-key",
170
+ envVar: params.defaultApiKeyEnvVar,
171
+ envVarName: params.defaultApiKeyEnvVar,
172
+ });
173
+ if (!resolved) {
174
+ return null;
175
+ }
176
+ const credential = buildSelfHostedProviderCredential({
177
+ ctx: params.ctx,
178
+ providerId: params.providerId,
179
+ resolved,
180
+ });
181
+ if (!credential) {
182
+ return null;
183
+ }
184
+ const configured = buildOpenAICompatibleSelfHostedProviderConfig({
185
+ cfg: params.ctx.config,
186
+ providerId: params.providerId,
187
+ baseUrl,
188
+ providerApiKey: params.defaultApiKeyEnvVar,
189
+ modelId,
190
+ input: params.input,
191
+ reasoning: params.reasoning,
192
+ contextWindow: params.contextWindow,
193
+ maxTokens: params.maxTokens,
194
+ });
195
+ await upsertAuthProfileWithLock({
196
+ profileId: configured.profileId,
197
+ credential,
198
+ agentDir: params.ctx.agentDir,
199
+ });
200
+ const withProfile = applyAuthProfileConfig(configured.config, {
201
+ profileId: configured.profileId,
202
+ provider: params.providerId,
203
+ mode: "api_key",
204
+ });
205
+ params.ctx.runtime.log(`Default ${params.providerLabel} model: ${modelId}`);
206
+ return applyProviderDefaultModel(withProfile, configured.modelRef);
207
+ }