akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
102 lines (101 loc) • 5.06 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* Loopback classification for LLM endpoints: does this endpoint name a model
* server running on THIS machine? A host is loopback when it is `localhost` or
* a `*.localhost` name (RFC 6761 §6.3), anywhere in `127.0.0.0/8`, an
* unspecified address (`0.0.0.0` / `::` — a client connecting there reaches
* local loopback), IPv6 `::1`, or the dotted IPv4-mapped spelling of a
* `127.0.0.0/8` address (`::ffff:127.0.0.1`). Address shapes are validated by
* `node:net`'s `isIP`, so a near-miss NAME like `127.0.0.1.evil.com` is remote.
*
* The check is purely syntactic — no DNS, no interface list — so the same
* config classifies the same on a laptop, on CI, and on a machine with no
* network. Ambiguity resolves toward LOOPBACK: misclassifying a local server
* as remote widens a concurrency pool onto a single-threaded local model
* (one loaded model, reload thrash, HTTP 500 — a hard failure); the reverse
* only costs throughput. The remedy in either direction is one line of config:
* `engines.<name>.concurrency`.
*
* The ONE local-vs-remote answer for concurrency defaults — the workflow
* engine's frozen engine concurrency (`workflows/concurrency-policy.ts`) and
* the embedding pool (`resolveEmbeddingConcurrency`,
* `src/llm/embedders/remote.ts`) both classify through here. NOT shared with
* the website snapshot fetcher's SSRF policy
* (`sources/snapshot-fetchers/host-guard.ts`), which deliberately draws
* different lines for a different threat model.
*
* @module core/loopback
*/
import { isIP } from "node:net";
/**
* The IPv4-mapped IPv6 spellings of `127.0.0.0/8`: the dotted form, whose
* capture is validated by `isIP`, and the hex form `::ffff:7fxx:xxxx`. Both are
* needed because WHATWG `URL` re-serializes `[::ffff:127.0.0.1]` to the hex
* form, so an endpoint written the dotted way arrives here as hex.
*/
const IPV4_MAPPED_IPV6 = /^::ffff:(?:([\d.]+)|7f[\da-f]{2}:[\da-f]{1,4})$/;
/** IPv6 groups before the last one, all of which must be zero (or elided). */
const ZERO_GROUP = /^0+$/;
/** The last IPv6 group of `::` / `::1`, in any zero-padding. */
const LOOPBACK_LAST_GROUP = /^0*[01]?$/;
/**
* True when `host` — a URL host component, with or without IPv6 brackets —
* names this machine WITHOUT resolving anything.
*
* Not recognized: the IPv4-compatible form (`::127.0.0.1`), and any NAME that
* merely resolves to loopback.
*
* Exported for the boundary-case table in
* `tests/workflows/concurrency-defaults.test.ts`, which has to reach the host
* predicate directly: several of its cases (a bare `""`, a malformed literal
* like `1::2::3`) cannot round-trip through a URL without changing meaning.
*/
export function isLoopbackHost(host) {
// Strip IPv6 brackets (`URL.hostname` keeps them) and the DNS root label. An
// empty host (`new URL("localhost:1234")` parses as scheme `localhost:` with
// no host) has nothing to judge, so it fails safe like an unparseable one.
const name = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
if (name === "" || name === "localhost" || name.endsWith(".localhost"))
return true;
const mapped = IPV4_MAPPED_IPV6.exec(name);
if (mapped) {
const quad = mapped[1];
// The hex branch already matched 127.x in the pattern; the dotted one still
// needs `isIP` so `::ffff:127.0.0.256` stays remote.
return quad === undefined || (isIP(quad) === 4 && quad.startsWith("127."));
}
const version = isIP(name);
if (version === 4)
return name.startsWith("127.") || name === "0.0.0.0";
if (version !== 6)
return false;
const groups = name.split(":");
const last = groups.pop() ?? "";
return groups.every((group) => group === "" || ZERO_GROUP.test(group)) && LOOPBACK_LAST_GROUP.test(last);
}
/** True when `endpoint` points at this machine (see {@link isLoopbackHost}). */
export function isLoopbackEndpoint(endpoint) {
if (!endpoint)
return true;
try {
return isLoopbackHost(new URL(endpoint).hostname);
}
catch {
// An unparseable endpoint is treated as local: guessing "remote" here would
// widen the pool on exactly the configs we understand least.
return true;
}
}
/**
* The lowest-common-denominator concurrency default for an LLM/embedding
* endpoint: 1 for a loopback endpoint (a local model server serves one
* inference at a time; concurrent requests thrash it — reload thrash, HTTP
* 500 "Model reloaded"), 2 for a remote one (enough to overlap request
* latency without hammering a rate-limited API). Used by the embedding pool
* (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`).
*/
export function defaultConcurrencyForEndpoint(endpoint) {
return isLoopbackEndpoint(endpoint) ? 1 : 2;
}