@gajae-code/coding-agent 0.18.11 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -15,6 +15,8 @@ import type { AuthStorage } from "@gajae-code/ai/core";
15
15
 
16
16
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
17
17
  import { SearchProviderError } from "../../../web/search/types";
18
+ import type { AddressResolver } from "../../insane/url-guard";
19
+ import { guardedPublicFetch, validatePublicHttpUrl } from "../../insane/url-guard";
18
20
  import { clampNumResults } from "../utils";
19
21
  import type { SearchParams } from "./base";
20
22
  import { SearchProvider } from "./base";
@@ -24,6 +26,8 @@ import { withHardTimeout } from "./utils";
24
26
  const DEFAULT_NUM_RESULTS = 10;
25
27
  const MAX_NUM_RESULTS = 20;
26
28
  const PUBLIC_ROUTE_TIMEOUT_MS = 15_000;
29
+ const MAX_PUBLIC_ROUTE_REDIRECTS = 5;
30
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
27
31
  const DISCOVERY_LIMIT = 8;
28
32
  const MAX_PUBLIC_ROUTE_RESPONSE_BYTES = 1024 * 1024;
29
33
 
@@ -151,6 +155,10 @@ class ResponseTooLargeError extends Error {
151
155
  override readonly name = "response_too_large";
152
156
  }
153
157
 
158
+ class PublicUrlRejectedError extends Error {
159
+ override readonly name = "public_url_rejected";
160
+ }
161
+
154
162
  async function cancelBody(body: ReadableStream<Uint8Array> | null): Promise<void> {
155
163
  if (!body) return;
156
164
  try {
@@ -203,17 +211,51 @@ async function readResponseText(response: Response): Promise<string> {
203
211
  async function fetchText(
204
212
  url: string,
205
213
  signal?: AbortSignal,
214
+ resolverOption?: { resolver?: AddressResolver },
206
215
  ): Promise<{ status: number; text: string; contentType: string }> {
207
- const response = await fetch(url, {
208
- headers: {
209
- Accept: "application/json, application/atom+xml, application/rss+xml, text/xml, text/html;q=0.8, */*;q=0.5",
210
- "User-Agent": USER_AGENT,
211
- },
212
- redirect: "follow",
213
- signal: withHardTimeout(signal, PUBLIC_ROUTE_TIMEOUT_MS),
214
- });
215
- const text = await readResponseText(response);
216
- return { status: response.status, text, contentType: response.headers.get("content-type") ?? "" };
216
+ const deadline = withHardTimeout(signal, PUBLIC_ROUTE_TIMEOUT_MS);
217
+ let currentUrl = url;
218
+
219
+ for (let redirectCount = 0; ; redirectCount++) {
220
+ // Fresh GET each hop: no body and no caller auth headers. guardedPublicFetch forces redirect:"manual".
221
+ const dial = await guardedPublicFetch(
222
+ currentUrl,
223
+ {
224
+ method: "GET",
225
+ headers: {
226
+ Accept:
227
+ "application/json, application/atom+xml, application/rss+xml, text/xml, text/html;q=0.8, */*;q=0.5",
228
+ "User-Agent": USER_AGENT,
229
+ },
230
+ redirect: "manual",
231
+ signal: deadline,
232
+ },
233
+ resolverOption,
234
+ );
235
+ if (!dial.ok) throw new PublicUrlRejectedError(dial.reason);
236
+
237
+ const { response, logicalUrl } = dial;
238
+ if (!REDIRECT_STATUSES.has(response.status)) {
239
+ const text = await readResponseText(response);
240
+ return { status: response.status, text, contentType: response.headers.get("content-type") ?? "" };
241
+ }
242
+
243
+ const location = response.headers.get("location")?.trim();
244
+ await cancelBody(response.body);
245
+ if (!location) throw new PublicUrlRejectedError("redirect missing Location");
246
+ if (redirectCount >= MAX_PUBLIC_ROUTE_REDIRECTS) throw new PublicUrlRejectedError("redirect limit exceeded");
247
+
248
+ let nextUrl: URL;
249
+ try {
250
+ nextUrl = new URL(location, logicalUrl);
251
+ } catch {
252
+ throw new PublicUrlRejectedError("invalid redirect Location");
253
+ }
254
+ const guard = await validatePublicHttpUrl(nextUrl.href, { signal: deadline, resolver: resolverOption?.resolver });
255
+ if (deadline.aborted) throw deadline.reason;
256
+ if (!guard.ok) throw new PublicUrlRejectedError(guard.reason);
257
+ currentUrl = guard.url.href;
258
+ }
217
259
  }
218
260
 
219
261
  function attr(input: string, name: string): string | undefined {
@@ -267,11 +309,15 @@ function redditFeedUrls(url: URL): string[] {
267
309
  return [`${base}/.rss`, `${base}.rss`];
268
310
  }
269
311
 
270
- async function routeReddit(url: URL, signal?: AbortSignal): Promise<RouteResult> {
312
+ async function routeReddit(
313
+ url: URL,
314
+ signal?: AbortSignal,
315
+ resolverOption?: { resolver?: AddressResolver },
316
+ ): Promise<RouteResult> {
271
317
  const attempts: InsaneRouteAttempt[] = [];
272
318
  for (const feedUrl of redditFeedUrls(url)) {
273
319
  try {
274
- const response = await fetchText(feedUrl, signal);
320
+ const response = await fetchText(feedUrl, signal, resolverOption);
275
321
  const ok = response.status === 200 && /<(feed|rss)\b/i.test(response.text) && !isBlockedBody(response.text);
276
322
  attempts.push(attempt("reddit", "rss", ok, response.status, response.text, ok ? "feed" : "no-feed-markers"));
277
323
  if (ok)
@@ -325,13 +371,17 @@ function sourceFromOEmbed(raw: string, url: string): SearchSource | null {
325
371
  return { title, url: data.url ?? url, snippet, author: data.author_name };
326
372
  }
327
373
 
328
- async function routeX(url: URL, signal?: AbortSignal): Promise<RouteResult> {
374
+ async function routeX(
375
+ url: URL,
376
+ signal?: AbortSignal,
377
+ resolverOption?: { resolver?: AddressResolver },
378
+ ): Promise<RouteResult> {
329
379
  const attempts: InsaneRouteAttempt[] = [];
330
380
  const id = tweetId(url);
331
381
  if (id) {
332
382
  const tweetResultUrl = `https://cdn.syndication.twimg.com/tweet-result?id=${encodeURIComponent(id)}&token=a`;
333
383
  try {
334
- const response = await fetchText(tweetResultUrl, signal);
384
+ const response = await fetchText(tweetResultUrl, signal, resolverOption);
335
385
  const source =
336
386
  response.status === 200 && !isBlockedBody(response.text)
337
387
  ? sourceFromTweetJson(response.text, url.toString())
@@ -346,7 +396,7 @@ async function routeX(url: URL, signal?: AbortSignal): Promise<RouteResult> {
346
396
  }
347
397
  const oembedUrl = `https://publish.twitter.com/oembed?url=${encodeURIComponent(`https://twitter.com/i/status/${id}`)}&omit_script=1`;
348
398
  try {
349
- const response = await fetchText(oembedUrl, signal);
399
+ const response = await fetchText(oembedUrl, signal, resolverOption);
350
400
  const source =
351
401
  response.status === 200 && !isBlockedBody(response.text)
352
402
  ? sourceFromOEmbed(response.text, oembedUrl)
@@ -363,7 +413,7 @@ async function routeX(url: URL, signal?: AbortSignal): Promise<RouteResult> {
363
413
  if (handle) {
364
414
  const timelineUrl = `https://syndication.twitter.com/srv/timeline-profile/screen-name/${encodeURIComponent(handle)}`;
365
415
  try {
366
- const response = await fetchText(timelineUrl, signal);
416
+ const response = await fetchText(timelineUrl, signal, resolverOption);
367
417
  const ok = response.status === 200 && response.text.includes("__NEXT_DATA__") && !isBlockedBody(response.text);
368
418
  attempts.push(
369
419
  attempt("x", "syndication-timeline", ok, response.status, response.text, ok ? "timeline" : "no-next-data"),
@@ -396,14 +446,18 @@ function youtubeVideoId(url: URL): string | null {
396
446
  return url.searchParams.get("v") ?? url.pathname.match(/\/shorts\/([^/?#]+)/)?.[1] ?? null;
397
447
  }
398
448
 
399
- async function routeYouTube(url: URL, signal?: AbortSignal): Promise<RouteResult> {
449
+ async function routeYouTube(
450
+ url: URL,
451
+ signal?: AbortSignal,
452
+ resolverOption?: { resolver?: AddressResolver },
453
+ ): Promise<RouteResult> {
400
454
  const attempts: InsaneRouteAttempt[] = [];
401
455
  const videoId = youtubeVideoId(url);
402
456
  if (videoId) {
403
457
  const watchUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
404
458
  const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json`;
405
459
  try {
406
- const response = await fetchText(oembedUrl, signal);
460
+ const response = await fetchText(oembedUrl, signal, resolverOption);
407
461
  const source =
408
462
  response.status === 200 && !isBlockedBody(response.text) ? sourceFromOEmbed(response.text, watchUrl) : null;
409
463
  attempts.push(
@@ -418,7 +472,7 @@ async function routeYouTube(url: URL, signal?: AbortSignal): Promise<RouteResult
418
472
  if (channelId) {
419
473
  const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${encodeURIComponent(channelId)}`;
420
474
  try {
421
- const response = await fetchText(feedUrl, signal);
475
+ const response = await fetchText(feedUrl, signal, resolverOption);
422
476
  const ok = response.status === 200 && /<feed\b/i.test(response.text) && !isBlockedBody(response.text);
423
477
  attempts.push(attempt("youtube", "feed", ok, response.status, response.text, ok ? "feed" : "no-feed"));
424
478
  if (ok)
@@ -466,13 +520,17 @@ function hnSourceFromItem(raw: string): SearchSource | null {
466
520
  };
467
521
  }
468
522
 
469
- async function routeHackerNews(url: URL, signal?: AbortSignal): Promise<RouteResult> {
523
+ async function routeHackerNews(
524
+ url: URL,
525
+ signal?: AbortSignal,
526
+ resolverOption?: { resolver?: AddressResolver },
527
+ ): Promise<RouteResult> {
470
528
  const attempts: InsaneRouteAttempt[] = [];
471
529
  const id = hnItemId(url);
472
530
  if (!id) return { platform: "hackernews", attempts };
473
531
  const itemUrl = `https://hacker-news.firebaseio.com/v0/item/${encodeURIComponent(id)}.json`;
474
532
  try {
475
- const response = await fetchText(itemUrl, signal);
533
+ const response = await fetchText(itemUrl, signal, resolverOption);
476
534
  const source = response.status === 200 && !isBlockedBody(response.text) ? hnSourceFromItem(response.text) : null;
477
535
  attempts.push(
478
536
  attempt("hackernews", "firebase-item", !!source, response.status, response.text, source ? "item" : "no-item"),
@@ -487,14 +545,18 @@ async function routeHackerNews(url: URL, signal?: AbortSignal): Promise<RouteRes
487
545
  return { platform: "hackernews", attempts };
488
546
  }
489
547
 
490
- export async function routeInsanePublicUrl(rawUrl: string, signal?: AbortSignal): Promise<RouteResult> {
548
+ export async function routeInsanePublicUrl(
549
+ rawUrl: string,
550
+ signal?: AbortSignal,
551
+ resolverOption?: { resolver?: AddressResolver },
552
+ ): Promise<RouteResult> {
491
553
  const url = parseHttpUrl(rawUrl);
492
554
  if (!url) return null;
493
555
  const platform = detectPlatform(url);
494
- if (platform === "reddit") return routeReddit(url, signal);
495
- if (platform === "x") return routeX(url, signal);
496
- if (platform === "youtube") return routeYouTube(url, signal);
497
- if (platform === "hackernews") return routeHackerNews(url, signal);
556
+ if (platform === "reddit") return routeReddit(url, signal, resolverOption);
557
+ if (platform === "x") return routeX(url, signal, resolverOption);
558
+ if (platform === "youtube") return routeYouTube(url, signal, resolverOption);
559
+ if (platform === "hackernews") return routeHackerNews(url, signal, resolverOption);
498
560
  return null;
499
561
  }
500
562
 
@@ -528,9 +590,10 @@ export async function searchInsane(params: {
528
590
  num_results?: number;
529
591
  recency?: "day" | "week" | "month" | "year";
530
592
  signal?: AbortSignal;
593
+ resolverOption?: { resolver?: AddressResolver };
531
594
  }): Promise<SearchResponse> {
532
595
  const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
533
- const direct = await routeInsanePublicUrl(params.query, params.signal);
596
+ const direct = await routeInsanePublicUrl(params.query, params.signal, params.resolverOption);
534
597
  if (isSuccess(direct) && direct.sources.length > 0) {
535
598
  return {
536
599
  provider: "insane",
@@ -560,7 +623,7 @@ export async function searchInsane(params: {
560
623
  const seenUrls = new Set<string>();
561
624
  for (const candidate of discovery.sources) {
562
625
  if (routedSources.length >= numResults) break;
563
- const routed = await routeInsanePublicUrl(candidate.url, params.signal);
626
+ const routed = await routeInsanePublicUrl(candidate.url, params.signal, params.resolverOption);
564
627
  if (routed && "attempts" in routed) attempts.push(...routed.attempts);
565
628
  if (!isSuccess(routed)) continue;
566
629
  for (const source of routed.sources) {
@@ -22,6 +22,11 @@
22
22
  * SEARXNG_BASIC_USERNAME - Optional RFC 7617 Basic auth username
23
23
  * SEARXNG_BASIC_PASSWORD - Optional RFC 7617 Basic auth password
24
24
  *
25
+ * A project settings endpoint must not receive a credential that exists only in
26
+ * the operator environment. The project file can still authenticate with a
27
+ * credential it sets itself, and an operator endpoint still receives an
28
+ * operator environment credential.
29
+ *
25
30
  * Reference: https://docs.searxng.org/dev/search_api.html
26
31
  */
27
32
 
@@ -80,7 +85,6 @@ interface SearXNGAuth {
80
85
  value: string;
81
86
  }
82
87
 
83
- /** Find SearXNG endpoint from settings or environment. */
84
88
  /**
85
89
  * SearXNG environment value, from trusted sources only, preserving an
86
90
  * intentionally empty setting.
@@ -122,6 +126,7 @@ export function resolveSearxngConfigForTest(): {
122
126
  };
123
127
  }
124
128
 
129
+ /** Find SearXNG endpoint from settings or environment. */
125
130
  function findEndpoint(activeSettings?: Settings): string | null {
126
131
  try {
127
132
  const endpoint = (activeSettings ?? settings).get("searxng.endpoint");
@@ -132,36 +137,66 @@ function findEndpoint(activeSettings?: Settings): string | null {
132
137
  return trustedSearxngEnv("SEARXNG_ENDPOINT");
133
138
  }
134
139
 
140
+ /**
141
+ * True when `endpoint` is the value supplied by the project settings layer.
142
+ *
143
+ * `get` is global, then project, then runtime override. `getOverride` and
144
+ * `getGlobal` are the layers a project file cannot write, so a value that is
145
+ * present, not overridden, and different from the global layer came from the
146
+ * project file. An equal global value is the operator endpoint even if the
147
+ * project file repeats it.
148
+ */
149
+ function endpointComesFromProjectLayer(activeSettings: Settings | undefined, endpoint: string | null): boolean {
150
+ if (!endpoint) return false;
151
+ try {
152
+ const active = activeSettings ?? settings;
153
+ if (active.getOverride("searxng.endpoint") !== undefined) return false;
154
+ if (!active.has("searxng.endpoint")) return false;
155
+ const effective = active.get("searxng.endpoint");
156
+ if (effective !== endpoint) return false;
157
+ const globalValue = active.getGlobal("searxng.endpoint");
158
+ return globalValue === undefined || globalValue !== effective;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
135
164
  /** Find SearXNG bearer token from settings or environment. */
136
- function findToken(activeSettings?: Settings): string | null {
165
+ function findToken(activeSettings?: Settings, allowEnvironment = true): string | null {
137
166
  try {
138
167
  const token = (activeSettings ?? settings).get("searxng.token");
139
168
  if (token) return token;
140
169
  } catch {
141
170
  // Settings not initialized yet
171
+ return allowEnvironment ? trustedSearxngEnv("SEARXNG_TOKEN") : null;
142
172
  }
173
+ if (!allowEnvironment) return null;
143
174
  return trustedSearxngEnv("SEARXNG_TOKEN");
144
175
  }
145
176
 
146
177
  /** Find SearXNG Basic auth username from settings or environment. */
147
- function findBasicUsername(activeSettings?: Settings): string | null {
178
+ function findBasicUsername(activeSettings?: Settings, allowEnvironment = true): string | null {
148
179
  try {
149
180
  const username = (activeSettings ?? settings).get("searxng.basicUsername");
150
181
  if (username !== undefined) return username;
151
182
  } catch {
152
183
  // Settings not initialized yet
184
+ return allowEnvironment ? trustedSearxngEnv("SEARXNG_BASIC_USERNAME") : null;
153
185
  }
186
+ if (!allowEnvironment) return null;
154
187
  return trustedSearxngEnv("SEARXNG_BASIC_USERNAME");
155
188
  }
156
189
 
157
190
  /** Find SearXNG Basic auth password from settings or environment. */
158
- function findBasicPassword(activeSettings?: Settings): string | null {
191
+ function findBasicPassword(activeSettings?: Settings, allowEnvironment = true): string | null {
159
192
  try {
160
193
  const password = (activeSettings ?? settings).get("searxng.basicPassword");
161
194
  if (password !== undefined) return password;
162
195
  } catch {
163
196
  // Settings not initialized yet
197
+ return allowEnvironment ? trustedSearxngEnv("SEARXNG_BASIC_PASSWORD") : null;
164
198
  }
199
+ if (!allowEnvironment) return null;
165
200
  return trustedSearxngEnv("SEARXNG_BASIC_PASSWORD");
166
201
  }
167
202
 
@@ -175,11 +210,26 @@ function hasControlCharacters(value: string): boolean {
175
210
  return /[\u0000-\u001F\u007F-\u009F]/u.test(value);
176
211
  }
177
212
 
178
- /** Find SearXNG authentication from settings or environment. Basic auth takes precedence over bearer tokens. */
179
- function findAuth(activeSettings?: Settings): SearXNGAuth | null {
180
- const basicUsername = findBasicUsername(activeSettings);
181
- const basicPassword = findBasicPassword(activeSettings);
182
- return resolveBasicOrBearerAuth(basicUsername, basicPassword, findToken(activeSettings));
213
+ /**
214
+ * Find SearXNG authentication from settings or environment. Basic auth takes
215
+ * precedence over bearer tokens.
216
+ *
217
+ * When `endpoint` came from the project settings layer, environment credentials
218
+ * are omitted. A complete basic-auth pair or token set in settings is still used,
219
+ * including one the project file set itself. An incomplete basic-auth pair is not
220
+ * filled from the environment: the search continues with a project token, or with
221
+ * no Authorization header, instead of failing closed on the missing half.
222
+ */
223
+ function findAuth(activeSettings?: Settings, endpoint?: string | null): SearXNGAuth | null {
224
+ const resolvedEndpoint = endpoint === undefined ? findEndpoint(activeSettings) : endpoint;
225
+ const allowEnvironment = !endpointComesFromProjectLayer(activeSettings, resolvedEndpoint);
226
+ const username = findBasicUsername(activeSettings, allowEnvironment);
227
+ const password = findBasicPassword(activeSettings, allowEnvironment);
228
+ const token = findToken(activeSettings, allowEnvironment);
229
+ if (!allowEnvironment && (username === null) !== (password === null)) {
230
+ return token ? { type: "bearer", value: token } : null;
231
+ }
232
+ return resolveBasicOrBearerAuth(username, password, token);
183
233
  }
184
234
 
185
235
  function resolveBasicOrBearerAuth(
@@ -322,7 +372,9 @@ export async function searchSearXNG(params: {
322
372
  );
323
373
  }
324
374
 
325
- const auth = params.authStorage ? await findAuthFromStorage(params.authStorage) : findAuth(params.settings);
375
+ const auth = params.authStorage
376
+ ? await findAuthFromStorage(params.authStorage)
377
+ : findAuth(params.settings, endpoint);
326
378
 
327
379
  let categories: string | undefined;
328
380
  let language: string | undefined;