Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,375 @@
// Google provider module implements model/runtime integration.
import {
createProviderHttpError,
formatProviderHttpErrorMessage,
readProviderJsonObjectResponse,
} from "openclaw/plugin-sdk/provider-http";
import {
buildSearchCacheKey,
buildUnsupportedSearchFilterResponse,
DEFAULT_SEARCH_COUNT,
MAX_SEARCH_COUNT,
parseWebSearchTimeFilters,
readCachedSearchPayload,
readConfiguredSecretString,
readPositiveIntegerParam,
readProviderEnvValue,
readStringParam,
resolveCitationRedirectUrl,
resolveSearchCacheTtlMs,
resolveSearchCount,
resolveSearchTimeoutSeconds,
type SearchConfigRecord,
withTrustedWebSearchEndpoint,
wrapWebContent,
writeCachedSearchPayload,
} from "openclaw/plugin-sdk/provider-web-search";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
resolveGeminiConfig,
resolveGeminiBaseUrl,
resolveGeminiModel,
type GeminiConfig,
} from "./gemini-web-search-provider.shared.js";
type GeminiFreshness = "day" | "week" | "month" | "year";
type GeminiTimeRangeFilter = {
startTime: string;
endTime: string;
};
type GeminiGroundingResponse = {
candidates?: Array<{
content?: {
parts?: Array<{
text?: string;
}>;
};
groundingMetadata?: {
groundingChunks?: Array<{
web?: {
uri?: string;
title?: string;
};
}>;
};
}>;
error?: {
code?: number;
message?: string;
status?: string;
};
};
function throwMalformedGeminiResponse(): never {
throw new Error("Gemini API error: malformed JSON response");
}
const GEMINI_FRESHNESS_DAYS: Record<GeminiFreshness, number> = {
day: 1,
week: 7,
month: 30,
year: 365,
};
const GEMINI_DAY_FRESHNESS_HINT = "Prioritize web sources published in the last 24 hours.";
// Gemini's google_search.time_range_filter accepts second-precision RFC 3339
// only. Despite the underlying google.protobuf.Timestamp type accepting "0, 3,
// 6 or 9 fractional digits", the Search grounding endpoint rejects any
// non-zero fractional component with
// "[FIELD_INVALID] Granularity of nano is not supported".
// Strip the fractional-second component before serializing.
function toGeminiTimeRangeTimestamp(date: Date): string {
return date.toISOString().replace(/\.\d+Z$/, "Z");
}
function isoDateStart(value: string): string {
return `${value}T00:00:00Z`;
}
function isoDateExclusiveEnd(value: string): string {
const end = new Date(`${value}T00:00:00Z`);
end.setUTCDate(end.getUTCDate() + 1);
return toGeminiTimeRangeTimestamp(end);
}
function freshnessStartTime(freshness: GeminiFreshness, now: Date): string {
const start = new Date(now);
start.setUTCDate(start.getUTCDate() - GEMINI_FRESHNESS_DAYS[freshness]);
return toGeminiTimeRangeTimestamp(start);
}
function queryWithSoftFreshness(query: string, freshness?: "day"): string {
if (freshness !== "day") {
return query;
}
return `${query}\n\nSearch recency instruction: ${GEMINI_DAY_FRESHNESS_HINT} If no matching recent sources are available, state that limitation and use the most relevant available sources.`;
}
function resolveGeminiTimeRangeFilter(
args: Record<string, unknown>,
now = new Date(),
):
| { timeRangeFilter?: GeminiTimeRangeFilter; freshness?: "day" }
| {
error:
| "invalid_freshness"
| "invalid_date"
| "invalid_date_range"
| "conflicting_time_filters";
message: string;
docs: string;
} {
const rawFreshness = readStringParam(args, "freshness");
const rawDateAfter = readStringParam(args, "date_after");
const rawDateBefore = readStringParam(args, "date_before");
const parsedTimeFilters = parseWebSearchTimeFilters({
rawDateAfter,
rawDateBefore,
rawFreshness,
freshnessProvider: "perplexity",
invalidFreshnessMessage:
"freshness must be day, week, month, year, or the shortcuts pd, pw, pm, py.",
invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
invalidDateRangeMessage: "date_after must be before date_before.",
});
if ("error" in parsedTimeFilters) {
return parsedTimeFilters;
}
const { freshness, dateAfter, dateBefore } = parsedTimeFilters;
if (freshness) {
// Gemini rejects 24-hour google_search.timeRangeFilter windows, while
// wider freshness windows still preserve the hard grounding contract.
if (freshness === "day") {
return {
freshness,
};
}
return {
timeRangeFilter: {
startTime: freshnessStartTime(freshness, now),
endTime: toGeminiTimeRangeTimestamp(now),
},
};
}
if (!dateAfter && !dateBefore) {
return {};
}
return {
timeRangeFilter: {
startTime: dateAfter ? isoDateStart(dateAfter) : "1970-01-01T00:00:00Z",
endTime: dateBefore ? isoDateExclusiveEnd(dateBefore) : toGeminiTimeRangeTimestamp(now),
},
};
}
export function resolveGeminiRuntimeApiKey(gemini?: GeminiConfig): string | undefined {
return (
readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ??
readProviderEnvValue(["GEMINI_API_KEY"]) ??
readConfiguredSecretString(gemini?.providerApiKey, "models.providers.google.apiKey")
);
}
async function runGeminiSearch(params: {
query: string;
apiKey: string;
baseUrl: string;
model: string;
timeoutSeconds: number;
signal?: AbortSignal;
timeRangeFilter?: GeminiTimeRangeFilter;
}): Promise<{ content: string; citations: Array<{ url: string; title?: string }> }> {
const endpoint = `${params.baseUrl}/models/${params.model}:generateContent`;
const googleSearch =
params.timeRangeFilter === undefined ? {} : { timeRangeFilter: params.timeRangeFilter };
return withTrustedWebSearchEndpoint(
{
url: endpoint,
timeoutSeconds: params.timeoutSeconds,
signal: params.signal,
init: {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": params.apiKey,
},
body: JSON.stringify({
contents: [{ parts: [{ text: params.query }] }],
tools: [{ google_search: googleSearch }],
}),
},
},
async (res) => {
if (!res.ok) {
const error = await createProviderHttpError(res, "Gemini API error");
throw new Error(error.message.replace(/key=[^&\s]+/giu, "key=***"));
}
const data = (await readProviderJsonObjectResponse(
res,
"Gemini API error",
)) as GeminiGroundingResponse;
if (data.error) {
const rawMessage = data.error.message || data.error.status || "unknown";
throw new Error(
formatProviderHttpErrorMessage({
label: "Gemini API error",
status: data.error.code ?? 0,
detail: rawMessage.replace(/key=[^&\s]+/giu, "key=***"),
}),
);
}
if (!Array.isArray(data.candidates)) {
throwMalformedGeminiResponse();
}
const candidate = data.candidates[0];
if (!isRecord(candidate) || !isRecord(candidate.content)) {
throwMalformedGeminiResponse();
}
const parts = candidate.content.parts;
if (!Array.isArray(parts)) {
throwMalformedGeminiResponse();
}
const content = parts
.map((part) => (isRecord(part) && typeof part.text === "string" ? part.text : undefined))
.filter((text): text is string => Boolean(text))
.join("\n");
if (!content) {
throwMalformedGeminiResponse();
}
const groundingMetadata = candidate.groundingMetadata;
const groundingChunks =
groundingMetadata === undefined
? []
: isRecord(groundingMetadata)
? groundingMetadata.groundingChunks === undefined
? []
: Array.isArray(groundingMetadata.groundingChunks)
? groundingMetadata.groundingChunks
: undefined
: undefined;
if (!groundingChunks) {
throwMalformedGeminiResponse();
}
const rawCitations = groundingChunks.flatMap((chunk) => {
if (!isRecord(chunk) || !isRecord(chunk.web) || typeof chunk.web.uri !== "string") {
return [];
}
return [
{
url: chunk.web.uri,
title: typeof chunk.web.title === "string" ? chunk.web.title : undefined,
},
];
});
const citations: Array<{ url: string; title?: string }> = [];
for (let index = 0; index < rawCitations.length; index += 10) {
const batch = rawCitations.slice(index, index + 10);
const resolved = await Promise.all(
batch.map(async (citation) =>
Object.assign({}, citation, { url: await resolveCitationRedirectUrl(citation.url) }),
),
);
citations.push(...resolved);
}
return { content, citations };
},
);
}
export async function executeGeminiSearch(
args: Record<string, unknown>,
searchConfig?: SearchConfigRecord,
context?: { signal?: AbortSignal },
): Promise<Record<string, unknown>> {
const unsupportedResponse = buildUnsupportedSearchFilterResponse(
{
country: args.country,
language: args.language,
},
"gemini",
);
if (unsupportedResponse) {
return unsupportedResponse;
}
const timeRange = resolveGeminiTimeRangeFilter(args);
if ("error" in timeRange) {
return timeRange;
}
const geminiConfig = resolveGeminiConfig(searchConfig);
const apiKey = resolveGeminiRuntimeApiKey(geminiConfig);
if (!apiKey) {
return {
error: "missing_gemini_api_key",
message:
"web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, configure plugins.entries.google.config.webSearch.apiKey, or reuse models.providers.google.apiKey. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
const query = readStringParam(args, "query", { required: true });
const count =
readPositiveIntegerParam(args, "count", {
max: MAX_SEARCH_COUNT,
message: `count must be an integer from 1 to ${MAX_SEARCH_COUNT}.`,
}) ??
searchConfig?.maxResults ??
undefined;
const model = resolveGeminiModel(geminiConfig);
const baseUrl = resolveGeminiBaseUrl(geminiConfig);
const cacheKey = buildSearchCacheKey([
"gemini",
query,
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
baseUrl,
model,
timeRange.freshness,
timeRange.timeRangeFilter?.startTime,
timeRange.timeRangeFilter?.endTime,
]);
const cached = readCachedSearchPayload(cacheKey);
if (cached) {
return cached;
}
const start = Date.now();
const result = await runGeminiSearch({
query: queryWithSoftFreshness(query, timeRange.freshness),
apiKey,
baseUrl,
model,
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
signal: context?.signal,
timeRangeFilter: timeRange.timeRangeFilter,
});
const payload = {
query,
provider: "gemini",
model,
tookMs: Date.now() - start,
externalContent: {
untrusted: true,
source: "web_search",
provider: "gemini",
wrapped: true,
},
content: wrapWebContent(result.content),
citations: result.citations,
};
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
return payload;
}