all repos — sz-chrome-remote-cdp @ master

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

castle-search/src/engine-target.ts (view raw)

 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(", ")}`;