🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@openafw/openafw

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@openafw/openafw - npm Package Compare versions

Comparing version
0.5.2
to
0.6.0
+418
dist/backends-BmyOv3bJ.js
import { atomicWrite, fileExists, paths } from "./secrets-evRw4cV3.js";
import { readFile } from "node:fs/promises";
//#region src/core/tool-providers.ts
const TOOL_PROVIDERS_VERSION = 1;
/** Seeded default — a keyless DuckDuckGo backend so first-run users
* have a working `web_search` without any setup. Quality is mediocre
* vs Brave/Tavily but it costs nothing and never asks for an account. */
const SEEDED_DDG = {
id: "ddg",
label: "DuckDuckGo (built-in)",
kind: "web_search",
backend: "duckduckgo",
origin: "seeded"
};
const EMPTY_STORE = {
version: TOOL_PROVIDERS_VERSION,
providers: [SEEDED_DDG],
active: {}
};
function isObj(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
const KINDS = ["web_search"];
const BACKENDS = [
"duckduckgo",
"brave",
"searxng",
"tavily",
"baidu"
];
function normalizeProvider(raw) {
if (!isObj(raw)) return void 0;
if (typeof raw.id !== "string" || raw.id === "") return void 0;
if (!KINDS.includes(raw.kind)) return void 0;
if (!BACKENDS.includes(raw.backend)) return void 0;
return {
id: raw.id,
label: typeof raw.label === "string" && raw.label !== "" ? raw.label : raw.id,
kind: raw.kind,
backend: raw.backend,
...typeof raw.baseUrl === "string" && raw.baseUrl !== "" ? { baseUrl: raw.baseUrl } : {},
...typeof raw.authRef === "string" && raw.authRef !== "" ? { authRef: raw.authRef } : {},
...typeof raw.costPerCall === "number" && raw.costPerCall >= 0 ? { costPerCall: raw.costPerCall } : {},
origin: raw.origin === "manual" ? "manual" : "seeded"
};
}
function normalizeToolProviders(raw) {
if (!isObj(raw)) return {
...EMPTY_STORE,
providers: [SEEDED_DDG]
};
const providers = Array.isArray(raw.providers) ? raw.providers.map(normalizeProvider).filter((p) => p != null) : [];
if (!providers.some((p) => p.kind === "web_search")) providers.push(SEEDED_DDG);
const active = {};
if (isObj(raw.active)) for (const k of KINDS) {
const v = raw.active[k];
if (typeof v === "string" && v !== "") active[k] = v;
}
return {
version: TOOL_PROVIDERS_VERSION,
providers,
active
};
}
async function readToolProviders() {
if (!await fileExists(paths.toolProviders)) return {
...EMPTY_STORE,
providers: [SEEDED_DDG]
};
return normalizeToolProviders(JSON.parse(await readFile(paths.toolProviders, "utf8")));
}
async function writeToolProviders(store) {
await atomicWrite(paths.toolProviders, `${JSON.stringify(store, null, 2)}\n`);
}
let writeChain = Promise.resolve();
/** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
function mutateToolProviders(fn) {
const next = writeChain.then(async () => {
const store = await readToolProviders();
const updated = fn(store);
if (updated) await writeToolProviders(updated);
return updated ?? store;
});
writeChain = next.catch(() => {});
return next;
}
/** Active provider for a kind — explicit `active[kind]` wins, then
* the first provider of that kind, then undefined (caller falls back
* to "no backend, return an error to the tool caller"). */
function activeProviderFor(store, kind) {
const explicit = store.active[kind];
if (explicit) {
const hit = store.providers.find((p) => p.id === explicit && p.kind === kind);
if (hit) return hit;
}
return store.providers.find((p) => p.kind === kind);
}
//#endregion
//#region src/core/web-search/backends.ts
const DDG_URL = "https://html.duckduckgo.com/html/";
const DDG_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
async function searchDuckDuckGo(opts) {
const form = new URLSearchParams({ q: opts.query });
let res;
try {
res = await fetch(DDG_URL, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
"user-agent": DDG_USER_AGENT,
accept: "text/html"
},
body: form.toString()
});
} catch (err) {
return {
ok: false,
error: `duckduckgo unreachable: ${err.message}`
};
}
if (!res.ok) return {
ok: false,
error: `duckduckgo HTTP ${res.status}`
};
const html = await res.text();
if (isDuckDuckGoAnomalyPage(html)) return {
ok: false,
error: "DuckDuckGo flagged this request as bot traffic and served a challenge page. This often resolves after a few minutes; for sustained use, switch to a key-based backend (Brave Search API) in Control · Tool Providers."
};
const results = parseDuckDuckGoHtml(html).slice(0, opts.count ?? 10);
return {
ok: true,
results
};
}
/** True when DDG returned its bot-challenge page instead of search
* results. The anomaly form's action URL is the strongest signal. */
function isDuckDuckGoAnomalyPage(html) {
return /anomaly\.js/.test(html) || /id="challenge-form"/.test(html);
}
/** Parse DDG's HTML results page. Their non-JS endpoint renders each
* result as a `result__a` anchor + `result__snippet` span. URLs come
* through a `//duckduckgo.com/l/?uddg=<encoded>` redirector — we
* decode the inner URL so callers get the real destination. The
* parser is intentionally a regex scan (no DOM lib) so it works in
* the daemon's plain node runtime; markup churn risk is the tradeoff. */
function parseDuckDuckGoHtml(html) {
const results = [];
const anchorRe = /<a[^>]+class="[^"]*\bresult__a\b[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
const snippetRe = /<(?:a|span)[^>]+class="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|span)>/g;
const snippets = [];
let s;
while ((s = snippetRe.exec(html)) !== null) snippets.push(stripHtml(s[1] ?? ""));
let i = 0;
let m;
while ((m = anchorRe.exec(html)) !== null) {
const rawHref = decodeHtmlEntities(m[1] ?? "");
const url = unwrapDdgRedirect(rawHref);
const title = stripHtml(m[2] ?? "");
if (!url || !title) {
i++;
continue;
}
const snippet = snippets[i];
results.push(snippet ? {
title,
url,
snippet
} : {
title,
url
});
i++;
}
return results;
}
function unwrapDdgRedirect(href) {
const m = /[?&]uddg=([^&]+)/.exec(href);
if (m?.[1]) try {
return decodeURIComponent(m[1]);
} catch {
return "";
}
if (href.startsWith("//")) return `https:${href}`;
if (href.startsWith("http")) return href;
return "";
}
function stripHtml(s) {
return decodeHtmlEntities(s.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
}
function decodeHtmlEntities(s) {
return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ");
}
const BRAVE_URL = "https://api.search.brave.com/res/v1/web/search";
async function searchBrave(opts) {
const url = new URL(BRAVE_URL);
url.searchParams.set("q", opts.query);
url.searchParams.set("count", String(opts.count ?? 10));
if (opts.locale) url.searchParams.set("country", opts.locale);
let res;
try {
res = await fetch(url, { headers: {
accept: "application/json",
"x-subscription-token": opts.apiKey
} });
} catch (err) {
return {
ok: false,
error: `brave unreachable: ${err.message}`
};
}
const text = await res.text();
if (!res.ok) return {
ok: false,
error: `brave HTTP ${res.status}: ${text.slice(0, 200)}`
};
let json;
try {
json = JSON.parse(text);
} catch {
return {
ok: false,
error: `brave returned non-JSON: ${text.slice(0, 200)}`
};
}
return {
ok: true,
results: parseBraveJson(json).slice(0, opts.count ?? 10)
};
}
function parseBraveJson(json) {
if (typeof json !== "object" || json === null) return [];
const web = json.web;
if (!web || !Array.isArray(web.results)) return [];
const out = [];
for (const r of web.results) {
if (typeof r !== "object" || r === null) continue;
const row = r;
const title = typeof row.title === "string" ? row.title : "";
const url = typeof row.url === "string" ? row.url : "";
const description = typeof row.description === "string" ? row.description : "";
if (!title || !url) continue;
out.push(description ? {
title,
url,
snippet: description
} : {
title,
url
});
}
return out;
}
const BAIDU_SMART_URL = "https://qianfan.baidubce.com/v2/ai_search/chat/completions";
const BAIDU_WEB_URL = "https://qianfan.baidubce.com/v2/ai_search/web_search";
const BAIDU_SMART_MODEL = "ernie-4.5-turbo-32k";
async function searchBaidu(opts) {
const smart = await searchBaiduSmart(opts);
if (smart.ok) return smart;
const plain = await searchBaiduWeb(opts);
if (plain.ok) return plain;
return {
ok: false,
error: `baidu smart-search failed (${smart.error}); plain web_search also failed (${plain.error})`
};
}
/** Smart search via /v2/ai_search/chat/completions. Carries a model
* parameter (the LLM summary is discarded by afw — the routed
* model synthesises its own answer — but the references[] field is
* the same shape we consume from plain web_search). */
async function searchBaiduSmart(opts) {
const count = Math.max(1, Math.min(20, opts.count ?? 10));
const body = {
messages: [{
role: "user",
content: opts.query
}],
model: BAIDU_SMART_MODEL,
search_source: "baidu_search_v2",
stream: false,
resource_type_filter: [{
type: "web",
top_k: count
}],
enable_reasoning: false,
enable_deep_search: false,
enable_followup_queries: false,
search_mode: "required"
};
const filter = freshnessToFilter(opts.freshness);
if (filter) body.search_filter = filter;
return callBaidu(BAIDU_SMART_URL, opts.apiKey, body, count);
}
/** Plain web search via /v2/ai_search/web_search. No model invocation,
* larger monthly quota — the fallback when smart search refuses. */
async function searchBaiduWeb(opts) {
const count = Math.max(1, Math.min(50, opts.count ?? 10));
const body = {
messages: [{
role: "user",
content: opts.query
}],
search_source: "baidu_search_v2",
resource_type_filter: [{
type: "web",
top_k: count
}]
};
const filter = freshnessToFilter(opts.freshness);
if (filter) body.search_filter = filter;
return callBaidu(BAIDU_WEB_URL, opts.apiKey, body, count);
}
async function callBaidu(url, apiKey, body, count) {
let res;
try {
res = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
authorization: `Bearer ${apiKey}`,
"x-appbuilder-from": "afw"
},
body: JSON.stringify(body)
});
} catch (err) {
return {
ok: false,
error: `baidu unreachable: ${err.message}`
};
}
const text = await res.text();
if (!res.ok) return {
ok: false,
error: `baidu HTTP ${res.status}: ${text.slice(0, 200)}`
};
let json;
try {
json = JSON.parse(text);
} catch {
return {
ok: false,
error: `baidu returned non-JSON: ${text.slice(0, 200)}`
};
}
if (json && typeof json === "object" && "code" in json && json.code) {
const msg = json.message;
return {
ok: false,
error: `baidu error: ${typeof msg === "string" ? msg : JSON.stringify(msg)}`
};
}
return {
ok: true,
results: parseBaiduJson(json).slice(0, count)
};
}
function parseBaiduJson(json) {
if (typeof json !== "object" || json === null) return [];
const refs = json.references;
if (!Array.isArray(refs)) return [];
const out = [];
for (const r of refs) {
if (typeof r !== "object" || r === null) continue;
const row = r;
const title = typeof row.title === "string" ? row.title : "";
const url = typeof row.url === "string" ? row.url : "";
const snippetField = typeof row.snippet === "string" ? row.snippet : typeof row.content === "string" ? row.content : "";
if (!title || !url) continue;
out.push(snippetField ? {
title,
url,
snippet: snippetField
} : {
title,
url
});
}
return out;
}
/** Map the Baidu `freshness` shorthand to its `search_filter` JSON.
* Returns undefined when freshness is absent or malformed (caller
* omits the filter so Baidu's default time range applies). */
function freshnessToFilter(freshness) {
if (!freshness) return void 0;
const now = new Date();
const dayShift = (n) => {
const d = new Date(now);
d.setUTCDate(d.getUTCDate() - n);
return d.toISOString().slice(0, 10);
};
const tomorrow = (() => {
const d = new Date(now);
d.setUTCDate(d.getUTCDate() + 1);
return d.toISOString().slice(0, 10);
})();
let start;
let end = tomorrow;
if (freshness === "pd") start = dayShift(1);
else if (freshness === "pw") start = dayShift(6);
else if (freshness === "pm") start = dayShift(30);
else if (freshness === "py") start = dayShift(364);
else {
const m = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/.exec(freshness);
if (!m) return void 0;
start = m[1];
end = m[2] ?? end;
}
return { range: { page_time: {
gte: start,
lt: end
} } };
}
//#endregion
export { activeProviderFor, mutateToolProviders, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo };
import { EMPTY_SECRETS, SECRETS_VERSION, getSecret, mutateSecrets, normalizeSecretStore, readSecrets, removeSecret, secretRefs, setSecret, writeSecrets } from "./secrets-evRw4cV3.js";
export { readSecrets };
import process from "node:process";
import { dirname, join } from "node:path";
import { access, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
//#region src/core/paths.ts
const HOME = homedir();
/** Resolve the afw home directory. Honors an explicit `AFW_HOME`
* override; otherwise `~/.afw`. */
function resolveHome() {
return process.env.AFW_HOME ?? join(HOME, ".afw");
}
const AFW_HOME = resolveHome();
const paths = {
home: AFW_HOME,
wire: {
dir: join(AFW_HOME, "wire"),
routes: join(AFW_HOME, "wire", "routes.json"),
traces: join(AFW_HOME, "wire", "traces"),
tracesArchive: join(AFW_HOME, "wire", "traces", "archive"),
daemonSock: join(AFW_HOME, "wire", "daemon.sock"),
daemonPid: join(AFW_HOME, "wire", "daemon.pid")
},
backups: {
dir: join(AFW_HOME, "backups"),
manifest: join(AFW_HOME, "backups", "manifest.json")
},
logs: {
dir: join(AFW_HOME, "logs"),
daemon: join(AFW_HOME, "logs", "daemon.log"),
daemonErr: join(AFW_HOME, "logs", "daemon.err")
},
config: join(AFW_HOME, "config.json"),
update: join(AFW_HOME, "update.json"),
models: join(AFW_HOME, "models.json"),
routing: join(AFW_HOME, "routing.json"),
secrets: join(AFW_HOME, "secrets.json"),
oauth: {
dir: join(AFW_HOME, "oauth"),
claudeCode: join(AFW_HOME, "oauth", "claude-code.json"),
codex: join(AFW_HOME, "oauth", "codex.json")
},
keys: join(AFW_HOME, "keys.json"),
tiers: join(AFW_HOME, "tiers.json"),
masking: join(AFW_HOME, "masking.json"),
toolProviders: join(AFW_HOME, "tool-providers.json"),
agent: {
claudeCode: {
settings: join(HOME, ".claude", "settings.json"),
legacy: join(HOME, ".claude.json")
},
claudeDesktop: {
root: join(HOME, "Library", "Application Support", "Claude"),
mcpConfig: join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")
},
openclaw: join(HOME, ".openclaw", "openclaw.json"),
opencode: join(HOME, ".config", "opencode", "opencode.json"),
hermes: {
config: join(HOME, ".hermes", "config.yaml"),
env: join(HOME, ".hermes", ".env")
},
codex: {
config: join(HOME, ".codex", "config.toml"),
auth: join(HOME, ".codex", "auth.json")
},
cursor: {
darwin: join(HOME, "Library", "Application Support", "Cursor", "User", "settings.json"),
linux: join(HOME, ".config", "Cursor", "User", "settings.json")
},
gemini: join(HOME, ".gemini", ".env")
}
};
const PRICING_OVERRIDE = join(AFW_HOME, "pricing.json");
const PRICING_CATALOG_CACHE = join(AFW_HOME, "pricing-catalog.json");
const DAEMON_PORT = (() => {
const p = process.env.AFW_PORT;
return p ? Number.parseInt(p, 10) : 9877;
})();
const DAEMON_HOST = "localhost";
const DAEMON_BASE_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
//#endregion
//#region src/core/atomic-file.ts
async function atomicWrite(path, content, opts) {
await mkdir(dirname(path), { recursive: true });
const tmp = `${path}.tmp.${process.pid}`;
await writeFile(tmp, content);
if (opts?.mode != null) await chmod(tmp, opts.mode);
await rename(tmp, path);
}
async function fileExists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
//#endregion
//#region src/core/secrets.ts
const SECRETS_VERSION = 1;
const SECRETS_MODE = 384;
const EMPTY_SECRETS = {
version: SECRETS_VERSION,
secrets: {}
};
function getSecret(store, ref) {
return store.secrets[ref];
}
/** Refs present in the store, for the UI to show which keys are configured
* (it never receives the values themselves). */
function secretRefs(store) {
return Object.keys(store.secrets);
}
function isObj(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function normalizeSecretStore(raw) {
if (!isObj(raw)) return { ...EMPTY_SECRETS };
if (raw.version !== SECRETS_VERSION) throw new Error(`secrets.json version ${String(raw.version)} not supported (expected ${SECRETS_VERSION})`);
const secrets = {};
if (isObj(raw.secrets)) {
for (const [ref, value] of Object.entries(raw.secrets)) if (typeof value === "string") secrets[ref] = value;
}
return {
version: SECRETS_VERSION,
secrets
};
}
async function readSecrets() {
if (!await fileExists(paths.secrets)) return { ...EMPTY_SECRETS };
return normalizeSecretStore(JSON.parse(await readFile(paths.secrets, "utf8")));
}
async function writeSecrets(store) {
await atomicWrite(paths.secrets, `${JSON.stringify(store, null, 2)}\n`, { mode: SECRETS_MODE });
}
let writeChain = Promise.resolve();
/** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
function mutateSecrets(fn) {
const next = writeChain.then(async () => {
const store = await readSecrets();
const updated = fn(store);
if (updated) await writeSecrets(updated);
return updated ?? store;
});
writeChain = next.catch(() => {});
return next;
}
/** Store a secret value under a ref. */
function setSecret(ref, value) {
return mutateSecrets((store) => ({
...store,
secrets: {
...store.secrets,
[ref]: value
}
}));
}
/** Remove a secret. No-op if the ref is absent. */
function removeSecret(ref) {
return mutateSecrets((store) => {
if (!(ref in store.secrets)) return void 0;
const secrets = { ...store.secrets };
delete secrets[ref];
return {
...store,
secrets
};
});
}
//#endregion
export { AFW_HOME, DAEMON_BASE_URL, DAEMON_PORT, EMPTY_SECRETS, PRICING_CATALOG_CACHE, PRICING_OVERRIDE, SECRETS_VERSION, atomicWrite, fileExists, getSecret, mutateSecrets, normalizeSecretStore, paths, readSecrets, removeSecret, secretRefs, setSecret, writeSecrets };

Sorry, the diff of this file is too big to display

+2
-2
#!/usr/bin/env node
import { getSecret, readSecrets } from "../secrets-Bj-gyv53.js";
import { activeProviderFor, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo } from "../backends-Byh5VYtT.js";
import { getSecret, readSecrets } from "../secrets-evRw4cV3.js";
import { activeProviderFor, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo } from "../backends-BmyOv3bJ.js";
import process from "node:process";

@@ -5,0 +5,0 @@

{
"name": "@openafw/openafw",
"version": "0.5.2",
"version": "0.6.0",
"description": "The local firewall for AI agents: route and repair them, and keep your secrets off the model, the API relay, and the supply chain. Local credential masking, per-route model routing, and security detectors on the wire. Free and fully open source.",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -1,4 +0,4 @@

# afw
# OpenAFW
> The local firewall for AI agents: route and repair them, and keep your
> The AI agent firewall that runs locally on your computer: route and fusion, and keep your
> secrets off the model, the API relay, and the supply chain.

@@ -5,0 +5,0 @@

@@ -7,3 +7,3 @@ <!doctype html>

<title>afw</title>
<script type="module" crossorigin src="/assets/index-C9yCeZlD.js"></script>
<script type="module" crossorigin src="/assets/index-Cja3pO9A.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BY6COSYk.css">

@@ -10,0 +10,0 @@ </head>

import { atomicWrite, fileExists, paths } from "./secrets-Bj-gyv53.js";
import { readFile } from "node:fs/promises";
//#region src/core/tool-providers.ts
const TOOL_PROVIDERS_VERSION = 1;
/** Seeded default — a keyless DuckDuckGo backend so first-run users
* have a working `web_search` without any setup. Quality is mediocre
* vs Brave/Tavily but it costs nothing and never asks for an account. */
const SEEDED_DDG = {
id: "ddg",
label: "DuckDuckGo (built-in)",
kind: "web_search",
backend: "duckduckgo",
origin: "seeded"
};
const EMPTY_STORE = {
version: TOOL_PROVIDERS_VERSION,
providers: [SEEDED_DDG],
active: {}
};
function isObj(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
const KINDS = ["web_search"];
const BACKENDS = [
"duckduckgo",
"brave",
"searxng",
"tavily",
"baidu"
];
function normalizeProvider(raw) {
if (!isObj(raw)) return void 0;
if (typeof raw.id !== "string" || raw.id === "") return void 0;
if (!KINDS.includes(raw.kind)) return void 0;
if (!BACKENDS.includes(raw.backend)) return void 0;
return {
id: raw.id,
label: typeof raw.label === "string" && raw.label !== "" ? raw.label : raw.id,
kind: raw.kind,
backend: raw.backend,
...typeof raw.baseUrl === "string" && raw.baseUrl !== "" ? { baseUrl: raw.baseUrl } : {},
...typeof raw.authRef === "string" && raw.authRef !== "" ? { authRef: raw.authRef } : {},
...typeof raw.costPerCall === "number" && raw.costPerCall >= 0 ? { costPerCall: raw.costPerCall } : {},
origin: raw.origin === "manual" ? "manual" : "seeded"
};
}
function normalizeToolProviders(raw) {
if (!isObj(raw)) return {
...EMPTY_STORE,
providers: [SEEDED_DDG]
};
const providers = Array.isArray(raw.providers) ? raw.providers.map(normalizeProvider).filter((p) => p != null) : [];
if (!providers.some((p) => p.kind === "web_search")) providers.push(SEEDED_DDG);
const active = {};
if (isObj(raw.active)) for (const k of KINDS) {
const v = raw.active[k];
if (typeof v === "string" && v !== "") active[k] = v;
}
return {
version: TOOL_PROVIDERS_VERSION,
providers,
active
};
}
async function readToolProviders() {
if (!await fileExists(paths.toolProviders)) return {
...EMPTY_STORE,
providers: [SEEDED_DDG]
};
return normalizeToolProviders(JSON.parse(await readFile(paths.toolProviders, "utf8")));
}
async function writeToolProviders(store) {
await atomicWrite(paths.toolProviders, `${JSON.stringify(store, null, 2)}\n`);
}
let writeChain = Promise.resolve();
/** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
function mutateToolProviders(fn) {
const next = writeChain.then(async () => {
const store = await readToolProviders();
const updated = fn(store);
if (updated) await writeToolProviders(updated);
return updated ?? store;
});
writeChain = next.catch(() => {});
return next;
}
/** Active provider for a kind — explicit `active[kind]` wins, then
* the first provider of that kind, then undefined (caller falls back
* to "no backend, return an error to the tool caller"). */
function activeProviderFor(store, kind) {
const explicit = store.active[kind];
if (explicit) {
const hit = store.providers.find((p) => p.id === explicit && p.kind === kind);
if (hit) return hit;
}
return store.providers.find((p) => p.kind === kind);
}
//#endregion
//#region src/core/web-search/backends.ts
const DDG_URL = "https://html.duckduckgo.com/html/";
const DDG_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
async function searchDuckDuckGo(opts) {
const form = new URLSearchParams({ q: opts.query });
let res;
try {
res = await fetch(DDG_URL, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
"user-agent": DDG_USER_AGENT,
accept: "text/html"
},
body: form.toString()
});
} catch (err) {
return {
ok: false,
error: `duckduckgo unreachable: ${err.message}`
};
}
if (!res.ok) return {
ok: false,
error: `duckduckgo HTTP ${res.status}`
};
const html = await res.text();
if (isDuckDuckGoAnomalyPage(html)) return {
ok: false,
error: "DuckDuckGo flagged this request as bot traffic and served a challenge page. This often resolves after a few minutes; for sustained use, switch to a key-based backend (Brave Search API) in Control · Tool Providers."
};
const results = parseDuckDuckGoHtml(html).slice(0, opts.count ?? 10);
return {
ok: true,
results
};
}
/** True when DDG returned its bot-challenge page instead of search
* results. The anomaly form's action URL is the strongest signal. */
function isDuckDuckGoAnomalyPage(html) {
return /anomaly\.js/.test(html) || /id="challenge-form"/.test(html);
}
/** Parse DDG's HTML results page. Their non-JS endpoint renders each
* result as a `result__a` anchor + `result__snippet` span. URLs come
* through a `//duckduckgo.com/l/?uddg=<encoded>` redirector — we
* decode the inner URL so callers get the real destination. The
* parser is intentionally a regex scan (no DOM lib) so it works in
* the daemon's plain node runtime; markup churn risk is the tradeoff. */
function parseDuckDuckGoHtml(html) {
const results = [];
const anchorRe = /<a[^>]+class="[^"]*\bresult__a\b[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
const snippetRe = /<(?:a|span)[^>]+class="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|span)>/g;
const snippets = [];
let s;
while ((s = snippetRe.exec(html)) !== null) snippets.push(stripHtml(s[1] ?? ""));
let i = 0;
let m;
while ((m = anchorRe.exec(html)) !== null) {
const rawHref = decodeHtmlEntities(m[1] ?? "");
const url = unwrapDdgRedirect(rawHref);
const title = stripHtml(m[2] ?? "");
if (!url || !title) {
i++;
continue;
}
const snippet = snippets[i];
results.push(snippet ? {
title,
url,
snippet
} : {
title,
url
});
i++;
}
return results;
}
function unwrapDdgRedirect(href) {
const m = /[?&]uddg=([^&]+)/.exec(href);
if (m?.[1]) try {
return decodeURIComponent(m[1]);
} catch {
return "";
}
if (href.startsWith("//")) return `https:${href}`;
if (href.startsWith("http")) return href;
return "";
}
function stripHtml(s) {
return decodeHtmlEntities(s.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
}
function decodeHtmlEntities(s) {
return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ");
}
const BRAVE_URL = "https://api.search.brave.com/res/v1/web/search";
async function searchBrave(opts) {
const url = new URL(BRAVE_URL);
url.searchParams.set("q", opts.query);
url.searchParams.set("count", String(opts.count ?? 10));
if (opts.locale) url.searchParams.set("country", opts.locale);
let res;
try {
res = await fetch(url, { headers: {
accept: "application/json",
"x-subscription-token": opts.apiKey
} });
} catch (err) {
return {
ok: false,
error: `brave unreachable: ${err.message}`
};
}
const text = await res.text();
if (!res.ok) return {
ok: false,
error: `brave HTTP ${res.status}: ${text.slice(0, 200)}`
};
let json;
try {
json = JSON.parse(text);
} catch {
return {
ok: false,
error: `brave returned non-JSON: ${text.slice(0, 200)}`
};
}
return {
ok: true,
results: parseBraveJson(json).slice(0, opts.count ?? 10)
};
}
function parseBraveJson(json) {
if (typeof json !== "object" || json === null) return [];
const web = json.web;
if (!web || !Array.isArray(web.results)) return [];
const out = [];
for (const r of web.results) {
if (typeof r !== "object" || r === null) continue;
const row = r;
const title = typeof row.title === "string" ? row.title : "";
const url = typeof row.url === "string" ? row.url : "";
const description = typeof row.description === "string" ? row.description : "";
if (!title || !url) continue;
out.push(description ? {
title,
url,
snippet: description
} : {
title,
url
});
}
return out;
}
const BAIDU_SMART_URL = "https://qianfan.baidubce.com/v2/ai_search/chat/completions";
const BAIDU_WEB_URL = "https://qianfan.baidubce.com/v2/ai_search/web_search";
const BAIDU_SMART_MODEL = "ernie-4.5-turbo-32k";
async function searchBaidu(opts) {
const smart = await searchBaiduSmart(opts);
if (smart.ok) return smart;
const plain = await searchBaiduWeb(opts);
if (plain.ok) return plain;
return {
ok: false,
error: `baidu smart-search failed (${smart.error}); plain web_search also failed (${plain.error})`
};
}
/** Smart search via /v2/ai_search/chat/completions. Carries a model
* parameter (the LLM summary is discarded by afw — the routed
* model synthesises its own answer — but the references[] field is
* the same shape we consume from plain web_search). */
async function searchBaiduSmart(opts) {
const count = Math.max(1, Math.min(20, opts.count ?? 10));
const body = {
messages: [{
role: "user",
content: opts.query
}],
model: BAIDU_SMART_MODEL,
search_source: "baidu_search_v2",
stream: false,
resource_type_filter: [{
type: "web",
top_k: count
}],
enable_reasoning: false,
enable_deep_search: false,
enable_followup_queries: false,
search_mode: "required"
};
const filter = freshnessToFilter(opts.freshness);
if (filter) body.search_filter = filter;
return callBaidu(BAIDU_SMART_URL, opts.apiKey, body, count);
}
/** Plain web search via /v2/ai_search/web_search. No model invocation,
* larger monthly quota — the fallback when smart search refuses. */
async function searchBaiduWeb(opts) {
const count = Math.max(1, Math.min(50, opts.count ?? 10));
const body = {
messages: [{
role: "user",
content: opts.query
}],
search_source: "baidu_search_v2",
resource_type_filter: [{
type: "web",
top_k: count
}]
};
const filter = freshnessToFilter(opts.freshness);
if (filter) body.search_filter = filter;
return callBaidu(BAIDU_WEB_URL, opts.apiKey, body, count);
}
async function callBaidu(url, apiKey, body, count) {
let res;
try {
res = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
authorization: `Bearer ${apiKey}`,
"x-appbuilder-from": "afw"
},
body: JSON.stringify(body)
});
} catch (err) {
return {
ok: false,
error: `baidu unreachable: ${err.message}`
};
}
const text = await res.text();
if (!res.ok) return {
ok: false,
error: `baidu HTTP ${res.status}: ${text.slice(0, 200)}`
};
let json;
try {
json = JSON.parse(text);
} catch {
return {
ok: false,
error: `baidu returned non-JSON: ${text.slice(0, 200)}`
};
}
if (json && typeof json === "object" && "code" in json && json.code) {
const msg = json.message;
return {
ok: false,
error: `baidu error: ${typeof msg === "string" ? msg : JSON.stringify(msg)}`
};
}
return {
ok: true,
results: parseBaiduJson(json).slice(0, count)
};
}
function parseBaiduJson(json) {
if (typeof json !== "object" || json === null) return [];
const refs = json.references;
if (!Array.isArray(refs)) return [];
const out = [];
for (const r of refs) {
if (typeof r !== "object" || r === null) continue;
const row = r;
const title = typeof row.title === "string" ? row.title : "";
const url = typeof row.url === "string" ? row.url : "";
const snippetField = typeof row.snippet === "string" ? row.snippet : typeof row.content === "string" ? row.content : "";
if (!title || !url) continue;
out.push(snippetField ? {
title,
url,
snippet: snippetField
} : {
title,
url
});
}
return out;
}
/** Map the Baidu `freshness` shorthand to its `search_filter` JSON.
* Returns undefined when freshness is absent or malformed (caller
* omits the filter so Baidu's default time range applies). */
function freshnessToFilter(freshness) {
if (!freshness) return void 0;
const now = new Date();
const dayShift = (n) => {
const d = new Date(now);
d.setUTCDate(d.getUTCDate() - n);
return d.toISOString().slice(0, 10);
};
const tomorrow = (() => {
const d = new Date(now);
d.setUTCDate(d.getUTCDate() + 1);
return d.toISOString().slice(0, 10);
})();
let start;
let end = tomorrow;
if (freshness === "pd") start = dayShift(1);
else if (freshness === "pw") start = dayShift(6);
else if (freshness === "pm") start = dayShift(30);
else if (freshness === "py") start = dayShift(364);
else {
const m = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/.exec(freshness);
if (!m) return void 0;
start = m[1];
end = m[2] ?? end;
}
return { range: { page_time: {
gte: start,
lt: end
} } };
}
//#endregion
export { activeProviderFor, mutateToolProviders, readToolProviders, searchBaidu, searchBrave, searchDuckDuckGo };
import { EMPTY_SECRETS, SECRETS_VERSION, getSecret, mutateSecrets, normalizeSecretStore, readSecrets, removeSecret, secretRefs, setSecret, writeSecrets } from "./secrets-Bj-gyv53.js";
export { readSecrets };
import process from "node:process";
import { dirname, join } from "node:path";
import { access, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
//#region src/core/paths.ts
const HOME = homedir();
/** Resolve the afw home directory. Honors an explicit `AFW_HOME`
* override; otherwise `~/.afw`. */
function resolveHome() {
return process.env.AFW_HOME ?? join(HOME, ".afw");
}
const AFW_HOME = resolveHome();
const paths = {
home: AFW_HOME,
wire: {
dir: join(AFW_HOME, "wire"),
routes: join(AFW_HOME, "wire", "routes.json"),
traces: join(AFW_HOME, "wire", "traces"),
tracesArchive: join(AFW_HOME, "wire", "traces", "archive"),
daemonSock: join(AFW_HOME, "wire", "daemon.sock"),
daemonPid: join(AFW_HOME, "wire", "daemon.pid")
},
backups: {
dir: join(AFW_HOME, "backups"),
manifest: join(AFW_HOME, "backups", "manifest.json")
},
logs: {
dir: join(AFW_HOME, "logs"),
daemon: join(AFW_HOME, "logs", "daemon.log"),
daemonErr: join(AFW_HOME, "logs", "daemon.err")
},
config: join(AFW_HOME, "config.json"),
update: join(AFW_HOME, "update.json"),
models: join(AFW_HOME, "models.json"),
routing: join(AFW_HOME, "routing.json"),
secrets: join(AFW_HOME, "secrets.json"),
keys: join(AFW_HOME, "keys.json"),
tiers: join(AFW_HOME, "tiers.json"),
masking: join(AFW_HOME, "masking.json"),
toolProviders: join(AFW_HOME, "tool-providers.json"),
agent: {
claudeCode: {
settings: join(HOME, ".claude", "settings.json"),
legacy: join(HOME, ".claude.json")
},
claudeDesktop: {
root: join(HOME, "Library", "Application Support", "Claude"),
mcpConfig: join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")
},
openclaw: join(HOME, ".openclaw", "openclaw.json"),
opencode: join(HOME, ".config", "opencode", "opencode.json"),
hermes: {
config: join(HOME, ".hermes", "config.yaml"),
env: join(HOME, ".hermes", ".env")
},
codex: {
config: join(HOME, ".codex", "config.toml"),
auth: join(HOME, ".codex", "auth.json")
},
cursor: {
darwin: join(HOME, "Library", "Application Support", "Cursor", "User", "settings.json"),
linux: join(HOME, ".config", "Cursor", "User", "settings.json")
},
gemini: join(HOME, ".gemini", ".env")
}
};
const PRICING_OVERRIDE = join(AFW_HOME, "pricing.json");
const PRICING_CATALOG_CACHE = join(AFW_HOME, "pricing-catalog.json");
const DAEMON_PORT = (() => {
const p = process.env.AFW_PORT;
return p ? Number.parseInt(p, 10) : 9877;
})();
const DAEMON_HOST = "localhost";
const DAEMON_BASE_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
//#endregion
//#region src/core/atomic-file.ts
async function atomicWrite(path, content, opts) {
await mkdir(dirname(path), { recursive: true });
const tmp = `${path}.tmp.${process.pid}`;
await writeFile(tmp, content);
if (opts?.mode != null) await chmod(tmp, opts.mode);
await rename(tmp, path);
}
async function fileExists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
//#endregion
//#region src/core/secrets.ts
const SECRETS_VERSION = 1;
const SECRETS_MODE = 384;
const EMPTY_SECRETS = {
version: SECRETS_VERSION,
secrets: {}
};
function getSecret(store, ref) {
return store.secrets[ref];
}
/** Refs present in the store, for the UI to show which keys are configured
* (it never receives the values themselves). */
function secretRefs(store) {
return Object.keys(store.secrets);
}
function isObj(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function normalizeSecretStore(raw) {
if (!isObj(raw)) return { ...EMPTY_SECRETS };
if (raw.version !== SECRETS_VERSION) throw new Error(`secrets.json version ${String(raw.version)} not supported (expected ${SECRETS_VERSION})`);
const secrets = {};
if (isObj(raw.secrets)) {
for (const [ref, value] of Object.entries(raw.secrets)) if (typeof value === "string") secrets[ref] = value;
}
return {
version: SECRETS_VERSION,
secrets
};
}
async function readSecrets() {
if (!await fileExists(paths.secrets)) return { ...EMPTY_SECRETS };
return normalizeSecretStore(JSON.parse(await readFile(paths.secrets, "utf8")));
}
async function writeSecrets(store) {
await atomicWrite(paths.secrets, `${JSON.stringify(store, null, 2)}\n`, { mode: SECRETS_MODE });
}
let writeChain = Promise.resolve();
/** Serialized read-modify-write — see model-registry.ts mutateModelRegistry. */
function mutateSecrets(fn) {
const next = writeChain.then(async () => {
const store = await readSecrets();
const updated = fn(store);
if (updated) await writeSecrets(updated);
return updated ?? store;
});
writeChain = next.catch(() => {});
return next;
}
/** Store a secret value under a ref. */
function setSecret(ref, value) {
return mutateSecrets((store) => ({
...store,
secrets: {
...store.secrets,
[ref]: value
}
}));
}
/** Remove a secret. No-op if the ref is absent. */
function removeSecret(ref) {
return mutateSecrets((store) => {
if (!(ref in store.secrets)) return void 0;
const secrets = { ...store.secrets };
delete secrets[ref];
return {
...store,
secrets
};
});
}
//#endregion
export { AFW_HOME, DAEMON_BASE_URL, DAEMON_PORT, EMPTY_SECRETS, PRICING_CATALOG_CACHE, PRICING_OVERRIDE, SECRETS_VERSION, atomicWrite, fileExists, getSecret, mutateSecrets, normalizeSecretStore, paths, readSecrets, removeSecret, secretRefs, setSecret, writeSecrets };

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display