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
|
/**
* Which search engine to use, resolved the same way the CDP host is:
* environment variable, then a persisted choice, then a built-in default.
*
* Pure — no I/O — so the `/search-engine` command can report the configured
* intent without touching disk twice, and so it is testable.
*/
import { type EngineId, parseEngineId, engineAliases } from "./engines.ts";
/** Used when nothing else says otherwise. */
export const DEFAULT_ENGINE: EngineId = "google";
/** The environment variable that pins the engine for one process. */
export const ENGINE_ENV_VAR = "CCS_SEARCH_ENGINE";
export type EngineSource = "parameter" | "env" | "stored" | "default";
export type EngineResolution =
| { readonly kind: "ok"; readonly id: EngineId; readonly source: EngineSource }
| { readonly kind: "invalid"; readonly raw: string; readonly source: EngineSource };
/**
* Precedence: the per-call tool parameter, then `CCS_SEARCH_ENGINE`, then the
* `/search-engine` choice, then the built-in default.
*
* Note the tool parameter sits *above* the environment variable, which is the
* opposite of how the CDP target treats an explicit endpoint. That difference
* is deliberate. For the browser, an environment variable has to win, because
* quietly driving a different machine means automating the operator's own
* signed-in Chrome — a safety property. Choosing a different search engine has
* no equivalent hazard, and the ability to fall back to another engine when the
* first one is serving a captcha is the single most useful thing the agent can
* do with this tool. So the model is allowed to override the default; the
* operator sets what it starts from.
*/
export const resolveEngine = (
parameter: string | null | undefined,
env: NodeJS.ProcessEnv = process.env,
stored: string | null = null,
): EngineResolution => {
const candidates: Array<{ raw: string; source: EngineSource }> = [];
const fromParam = parameter?.trim();
if (fromParam) candidates.push({ raw: fromParam, source: "parameter" });
const fromEnv = env[ENGINE_ENV_VAR]?.trim();
if (fromEnv) candidates.push({ raw: fromEnv, source: "env" });
const fromStore = stored?.trim();
if (fromStore) candidates.push({ raw: fromStore, source: "stored" });
for (const candidate of candidates) {
const id = parseEngineId(candidate.raw);
// An unparseable value is reported rather than skipped over. Falling
// through to the next source would mean a typo in CCS_SEARCH_ENGINE
// silently searches Google instead of saying so.
if (!id) return { kind: "invalid", raw: candidate.raw, source: candidate.source };
return { kind: "ok", id, source: candidate.source };
}
return { kind: "ok", id: DEFAULT_ENGINE, source: "default" };
};
const sourceLabel = (source: EngineSource): string =>
source === "parameter"
? "engine parameter"
: source === "env"
? ENGINE_ENV_VAR
: source === "stored"
? "/search-engine"
: "built-in default";
export const describeEngineSource = (source: EngineSource): string => sourceLabel(source);
/** One-line description of a bad engine name, listing what would have worked. */
export const describeEngineFailure = (failure: Extract<EngineResolution, { kind: "invalid" }>): string =>
`unknown search engine "${failure.raw}" from ${sourceLabel(failure.source)} — ` +
`expected one of ${engineAliases().join(", ")}`;
|