loopctl-mcp-server 2.52.0 → 2.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/index.js +309 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
|
|
|
121
121
|
autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
|
|
122
122
|
[`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
|
|
123
123
|
|
|
124
|
-
## Tools (
|
|
124
|
+
## Tools (109)
|
|
125
125
|
|
|
126
126
|
> Plus **per-tenant generated Context Retriever tools** (`cr_*`) appended
|
|
127
127
|
> dynamically at runtime — see [Dynamic per-tenant Context Retriever
|
|
@@ -297,7 +297,7 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
|
|
|
297
297
|
| Tool | Description |
|
|
298
298
|
|---|---|
|
|
299
299
|
| `llm_config` | **Check your onboarding status.** Get the tenant's BYO LLM config: per-operation models and whether each key is set — `has_api_key` (Anthropic) / `has_embedding_key` (OpenAI embedding) — plus masked last-4 hints. Never returns a key. Requires **user** key. |
|
|
300
|
-
| `set_llm_config` | **First-time setup (do once).** Set/rotate the tenant's OWN **Anthropic `api_key`** (powers ingest) AND **OpenAI `embedding_api_key`** (powers semantic search) — both stored encrypted, never returned — plus the per-operation models (`extraction_model`/`classification_model`/`merge_model`/`embedding_model`). Any subset; partial-merge (omitting a key leaves it untouched). See [First-time setup](#first-time-setup--provision-your-byo-llm-keys). Requires **user** key. |
|
|
300
|
+
| `set_llm_config` | **First-time setup (do once).** Set/rotate the tenant's OWN **Anthropic `api_key`** (powers ingest) AND **OpenAI `embedding_api_key`** (powers semantic search) — both stored encrypted, never returned — plus the per-operation models (`extraction_model`/`classification_model`/`merge_model`/`embedding_model`). Any subset; partial-merge (omitting a key leaves it untouched). **Private tier (US-41.3):** also sets `chat_provider` (`anthropic` | `openai_compatible`), `chat_base_url`, `extraction_model` (required on that provider) and `chat_api_key` (optional — omit it for a keyless local server) so ingest extraction / classification / merge / memory promotion run against YOUR OWN OpenAI-compatible endpoint and document text never leaves your boundary. Every model the config resolves to is PROBED with a trivial completion before it is saved (a 422 persists nothing); the probe re-runs on any chat-surface change (provider, base_url, key rotation, model), a private-range host is refused by the SSRF guard, and moving to a NEW host needs a matching `chat_api_key` or `acknowledge_key_transmission: true`. See [First-time setup](#first-time-setup--provision-your-byo-llm-keys). Requires **user** key. |
|
|
301
301
|
| `knowledge_llm_usage` | Per-tenant LLM token-usage summary, grouped by operation + model + source_type + day over an optional `from`/`to` range (defaults to a 90-day lookback; effective window echoed in `meta.from`/`meta.to`), with `limit`/`offset` pagination. Record-only (no budget enforcement). Requires orchestrator key. |
|
|
302
302
|
|
|
303
303
|
### Knowledge Analytics Tools (orchestrator key)
|
|
@@ -311,6 +311,20 @@ it is enforced server-side and a no-op for a non-superadmin key — see below.)
|
|
|
311
311
|
| `knowledge_curation_log` | Concise human-readable log of KB CURATION adjustments — novelty-gate decisions (`gate_duplicate`/`gate_draft`) and conflict resolutions (`supersede`/`merge`/`dismiss`) — for analyzing the agents'-KB rollout, distinct from the verbose audit log. Each entry: `{at, kind, summary, refs, actor, confidence}`. **RECORDED ONLY when `settings.kb_curation_log` is on** (PATCH `/api/v1/admin/tenants/:id` with `settings:{kb_curation_log:true}`); off by default = no rows. Most recent first. Requires orchestrator role. Optional: `kind`, `since` (ISO8601), `limit` (default 50, max 500), `offset`. |
|
|
312
312
|
| `knowledge_retrieval_metrics` | Daily retrieval-PRECISION time series: for each day, the share of search results the agent then opened (search → get/context within a window). A proxy for whether retrieval is improving as the corpus is de-duplicated, better navigated (MOCs), and conflict-resolved. Most recent day first. Requires orchestrator role. Optional: `limit` (default 30, max 365), `offset`. |
|
|
313
313
|
|
|
314
|
+
### Egress / Privacy Tools (US-41.4)
|
|
315
|
+
|
|
316
|
+
The fail-closed no-egress guard. `local_only` is **OFF by default everywhere** —
|
|
317
|
+
nothing changes until a scope opts in.
|
|
318
|
+
|
|
319
|
+
| Tool | Description |
|
|
320
|
+
|---|---|
|
|
321
|
+
| `egress_posture` | **VERIFY BEFORE YOU HARVEST.** This instance's egress posture for your tenant: the resolved embedding + chat endpoints with a locality VERDICT for each (`network-local` / `tenant-declared (unverified attestation), not network-local` / `non-local`), your declared trusted endpoints and their purposes, per-scope `local_only` / `encrypt_body`, and any named posture defects. Endpoints are shown; **keys never are**. Deployment-allowlist CONTENTS appear only at **user**+ — at agent role each endpoint carries only a boolean saying whether its verdict came from the allowlist. Agent key. |
|
|
322
|
+
| `set_local_only` | **TIGHTEN**: mark a scope `local_only`, so loopctl HARD-REFUSES any model-provider call whose resolved endpoint is not classified local. Scope resolution is MOST-RESTRICTIVE-WINS (project OR tenant); a project can never relax a tenant marking. MANDATORY PRE-FLIGHT: refused with **409** `would_block_endpoints` naming every endpoint that would become `egress_blocked`, unless you pass `acknowledge: true`. Requires **orchestrator** key — tightening is safe to automate. |
|
|
323
|
+
| `clear_local_only` | **WIDEN**: remove a scope's `local_only` marking. Requires your **user** key (`LOOPCTL_USER_KEY`) — deliberately not available to an agent or orchestrator, because clearing is the self-widening move an automated key must never make one call before a harvest. Audited with actor + scope. |
|
|
324
|
+
| `declare_trusted_endpoint` | Declare a host you attest is YOUR OWN so a `local_only` scope can reach it. **This is an unverified tenant attestation, not network locality** — labelled as such everywhere. Three enforced constraints: PUBLIC addresses only (write time AND pin time), PURPOSE-SCOPED (`inference` / `webhook` / `ingest`), vendor hosts excluded. Carves nothing out of the SSRF denylist. Requires **user** key. |
|
|
325
|
+
| `revoke_trusted_endpoint` | Revoke a declaration. Invalidation is IMMEDIATE and cluster-wide — a revoked declaration does not keep working for the remainder of the pin TTL. Requires **user** key. |
|
|
326
|
+
| `egress_repin` | Recover from a `:pin_stale` error (your box got a new DHCP lease and the pinned address set changed — DISTINCT from `egress_blocked`). Re-resolves and re-pins the host. **Agent** key by design: requiring a human user-role write to recover would contradict loopctl's agent-native, no-UI design. |
|
|
327
|
+
|
|
314
328
|
### Discovery Tools
|
|
315
329
|
|
|
316
330
|
| Tool | Description |
|
package/index.js
CHANGED
|
@@ -1852,6 +1852,10 @@ async function setLlmConfig({
|
|
|
1852
1852
|
merge_model,
|
|
1853
1853
|
embedding_api_key,
|
|
1854
1854
|
embedding_model,
|
|
1855
|
+
chat_provider,
|
|
1856
|
+
chat_base_url,
|
|
1857
|
+
chat_api_key,
|
|
1858
|
+
acknowledge_key_transmission,
|
|
1855
1859
|
}) {
|
|
1856
1860
|
const body = {};
|
|
1857
1861
|
if (api_key != null) body.api_key = api_key;
|
|
@@ -1860,6 +1864,15 @@ async function setLlmConfig({
|
|
|
1860
1864
|
if (merge_model !== undefined) body.merge_model = merge_model;
|
|
1861
1865
|
if (embedding_api_key != null) body.embedding_api_key = embedding_api_key;
|
|
1862
1866
|
if (embedding_model !== undefined) body.embedding_model = embedding_model;
|
|
1867
|
+
// US-41.3: pluggable OpenAI-compatible chat endpoint. The server PROBES the
|
|
1868
|
+
// endpoint with a trivial completion before persisting, and refuses an endpoint
|
|
1869
|
+
// change that carries neither a matching chat_api_key nor an explicit
|
|
1870
|
+
// acknowledge_key_transmission.
|
|
1871
|
+
if (chat_provider !== undefined) body.chat_provider = chat_provider;
|
|
1872
|
+
if (chat_base_url !== undefined) body.chat_base_url = chat_base_url;
|
|
1873
|
+
if (chat_api_key != null) body.chat_api_key = chat_api_key;
|
|
1874
|
+
if (acknowledge_key_transmission === true)
|
|
1875
|
+
body.acknowledge_key_transmission = true;
|
|
1863
1876
|
// PATCH (partial-merge) + EXACT user key (review #12, #13).
|
|
1864
1877
|
const result = await apiCall(
|
|
1865
1878
|
"PATCH",
|
|
@@ -1871,6 +1884,88 @@ async function setLlmConfig({
|
|
|
1871
1884
|
return toContent(result);
|
|
1872
1885
|
}
|
|
1873
1886
|
|
|
1887
|
+
// --- US-41.4: fail-closed no-egress guard -----------------------------------
|
|
1888
|
+
//
|
|
1889
|
+
// egress_posture is a READ available at AGENT role: an agent must be able to
|
|
1890
|
+
// verify locality with the key it already has, or the whole verify-before-harvest
|
|
1891
|
+
// workflow fails. WRITING local_only is asymmetric: ENABLE (tightening) is
|
|
1892
|
+
// orchestrator+, CLEAR (widening) is user-only, and declaring a trusted endpoint
|
|
1893
|
+
// is user-only. Re-pinning is agent-role on purpose — a home Ollama box or a DHCP
|
|
1894
|
+
// VPS changes IP routinely and recovery must not need a human.
|
|
1895
|
+
async function egressPosture() {
|
|
1896
|
+
const result = await apiCall(
|
|
1897
|
+
"GET",
|
|
1898
|
+
"/api/v1/egress/posture",
|
|
1899
|
+
null,
|
|
1900
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1901
|
+
);
|
|
1902
|
+
return toContent(result);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
async function setLocalOnly({ project_id, acknowledge } = {}) {
|
|
1906
|
+
const body = {};
|
|
1907
|
+
if (project_id != null) body.project_id = project_id;
|
|
1908
|
+
if (acknowledge === true) body.acknowledge = true;
|
|
1909
|
+
// ENABLE = tightening = orchestrator+.
|
|
1910
|
+
const result = await apiCall(
|
|
1911
|
+
"POST",
|
|
1912
|
+
"/api/v1/egress/local-only",
|
|
1913
|
+
body,
|
|
1914
|
+
process.env.LOOPCTL_ORCH_KEY,
|
|
1915
|
+
);
|
|
1916
|
+
return toContent(result);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
async function clearLocalOnly({ project_id } = {}) {
|
|
1920
|
+
// CLEAR = self-widening = EXACT user key. An agent must never be able to
|
|
1921
|
+
// re-open egress one tool call before a harvest.
|
|
1922
|
+
const qs = project_id ? `?project_id=${encodeURIComponent(project_id)}` : "";
|
|
1923
|
+
const result = await apiCall(
|
|
1924
|
+
"DELETE",
|
|
1925
|
+
`/api/v1/egress/local-only${qs}`,
|
|
1926
|
+
null,
|
|
1927
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1928
|
+
{ exactKey: true },
|
|
1929
|
+
);
|
|
1930
|
+
return toContent(result);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
async function declareTrustedEndpoint({ host, purposes, note } = {}) {
|
|
1934
|
+
const body = { host, purposes };
|
|
1935
|
+
if (note != null) body.note = note;
|
|
1936
|
+
const result = await apiCall(
|
|
1937
|
+
"POST",
|
|
1938
|
+
"/api/v1/egress/trusted-endpoints",
|
|
1939
|
+
body,
|
|
1940
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1941
|
+
{ exactKey: true },
|
|
1942
|
+
);
|
|
1943
|
+
return toContent(result);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
async function revokeTrustedEndpoint({ host } = {}) {
|
|
1947
|
+
const result = await apiCall(
|
|
1948
|
+
"DELETE",
|
|
1949
|
+
`/api/v1/egress/trusted-endpoints/${encodeURIComponent(host)}`,
|
|
1950
|
+
null,
|
|
1951
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1952
|
+
{ exactKey: true },
|
|
1953
|
+
);
|
|
1954
|
+
return toContent(result);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
async function egressRepin({ host, project_id } = {}) {
|
|
1958
|
+
const body = { host };
|
|
1959
|
+
if (project_id != null) body.project_id = project_id;
|
|
1960
|
+
const result = await apiCall(
|
|
1961
|
+
"POST",
|
|
1962
|
+
"/api/v1/egress/repin",
|
|
1963
|
+
body,
|
|
1964
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1965
|
+
);
|
|
1966
|
+
return toContent(result);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1874
1969
|
async function knowledgeLlmUsage(args = {}) {
|
|
1875
1970
|
// Query-string building lives in lib/http-helpers.js so the test suite exercises
|
|
1876
1971
|
// the same from/to/limit/offset logic the server ships.
|
|
@@ -4863,6 +4958,145 @@ const TOOLS = [
|
|
|
4863
4958
|
},
|
|
4864
4959
|
|
|
4865
4960
|
// Per-tenant BYO LLM config + usage (Epic 28 residual, #179)
|
|
4961
|
+
{
|
|
4962
|
+
name: "egress_posture",
|
|
4963
|
+
description:
|
|
4964
|
+
"VERIFY BEFORE YOU HARVEST. Reports this instance's egress posture for YOUR tenant: " +
|
|
4965
|
+
"the resolved embedding and chat endpoints, a locality VERDICT for each " +
|
|
4966
|
+
"(network-local / 'tenant-declared (unverified attestation), not network-local' / " +
|
|
4967
|
+
"non-local), your declared trusted endpoints with their purposes, per-scope " +
|
|
4968
|
+
"local_only status, and any named posture defects. Endpoints are shown; KEYS NEVER " +
|
|
4969
|
+
"ARE. Call this BEFORE sending private documents anywhere, instead of trusting that " +
|
|
4970
|
+
"the operator configured things correctly. READ tool, available at AGENT role — the " +
|
|
4971
|
+
"key you already have. NOTE the deployment allowlist CONTENTS are operator " +
|
|
4972
|
+
"infrastructure and are NOT disclosed at agent role: you get only a boolean per " +
|
|
4973
|
+
"endpoint saying whether its verdict came from the allowlist (contents at user+). " +
|
|
4974
|
+
"SCOPE OF THE GUARANTEE: fail-closed enforcement covers every outbound HTTP call " +
|
|
4975
|
+
"made by loopctl application code on the MODEL-PROVIDER path. Webhook delivery is " +
|
|
4976
|
+
"not covered yet (US-41.5), and HTTP performed inside a dependency, plus this " +
|
|
4977
|
+
"separate mcp-server codebase, are outside the static chokepoint check.",
|
|
4978
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
4979
|
+
},
|
|
4980
|
+
{
|
|
4981
|
+
name: "set_local_only",
|
|
4982
|
+
description:
|
|
4983
|
+
"TIGHTEN the posture: mark a scope local_only so loopctl HARD-REFUSES any " +
|
|
4984
|
+
"model-provider call whose resolved endpoint is not classified local. Default is OFF " +
|
|
4985
|
+
"everywhere; nothing changes until a scope opts in. Scope resolution is " +
|
|
4986
|
+
"MOST-RESTRICTIVE-WINS (project OR tenant) and a project can NEVER relax a tenant " +
|
|
4987
|
+
"marking. MANDATORY PRE-FLIGHT: the call is REFUSED with 409 would_block_endpoints, " +
|
|
4988
|
+
"naming every endpoint that would become egress_blocked, unless you pass " +
|
|
4989
|
+
"acknowledge: true — because on a tenant still using vendor default endpoints this " +
|
|
4990
|
+
"instantly stops embedding, extraction, classification and merge, and only a " +
|
|
4991
|
+
"human user-role key can undo it. Requires an ORCHESTRATOR key: tightening is safe " +
|
|
4992
|
+
"to automate. CLEARING is a different tool (clear_local_only) and is user-only.",
|
|
4993
|
+
inputSchema: {
|
|
4994
|
+
type: "object",
|
|
4995
|
+
properties: {
|
|
4996
|
+
project_id: {
|
|
4997
|
+
type: "string",
|
|
4998
|
+
description:
|
|
4999
|
+
"Mark this PROJECT only (omit to mark the whole tenant). A project-less row — " +
|
|
5000
|
+
"a tenant-wide article, any memory — always follows the TENANT marking.",
|
|
5001
|
+
},
|
|
5002
|
+
acknowledge: {
|
|
5003
|
+
type: "boolean",
|
|
5004
|
+
description:
|
|
5005
|
+
"Accept the reported blocked posture and proceed. Required when the pre-flight " +
|
|
5006
|
+
"finds endpoints that would become egress_blocked.",
|
|
5007
|
+
},
|
|
5008
|
+
},
|
|
5009
|
+
required: [],
|
|
5010
|
+
},
|
|
5011
|
+
},
|
|
5012
|
+
{
|
|
5013
|
+
name: "clear_local_only",
|
|
5014
|
+
description:
|
|
5015
|
+
"WIDEN the posture: remove a scope's local_only marking, re-permitting non-local " +
|
|
5016
|
+
"model-provider egress for it. Requires your EXACT user-role key " +
|
|
5017
|
+
"(LOOPCTL_USER_KEY) — deliberately NOT available to an agent or an orchestrator, " +
|
|
5018
|
+
"because clearing is the self-widening move that would otherwise let an automated " +
|
|
5019
|
+
"key re-open egress immediately before a harvest. Audited with actor and scope.",
|
|
5020
|
+
inputSchema: {
|
|
5021
|
+
type: "object",
|
|
5022
|
+
properties: {
|
|
5023
|
+
project_id: {
|
|
5024
|
+
type: "string",
|
|
5025
|
+
description: "Clear this PROJECT's marking (omit to clear the tenant marking).",
|
|
5026
|
+
},
|
|
5027
|
+
},
|
|
5028
|
+
required: [],
|
|
5029
|
+
},
|
|
5030
|
+
},
|
|
5031
|
+
{
|
|
5032
|
+
name: "declare_trusted_endpoint",
|
|
5033
|
+
description:
|
|
5034
|
+
"Declare a host you attest is YOUR OWN (your VPS, tailscale funnel, or domain) so a " +
|
|
5035
|
+
"local_only scope can reach it. THIS IS AN UNVERIFIED TENANT ATTESTATION, NOT " +
|
|
5036
|
+
"NETWORK LOCALITY: loopctl does not prove you own the host, and the posture report " +
|
|
5037
|
+
"and custody claim label it 'tenant-declared (unverified attestation), not " +
|
|
5038
|
+
"network-local' — never as network-local. THREE ENFORCED CONSTRAINTS: (1) PUBLIC " +
|
|
5039
|
+
"ADDRESSES ONLY — a host that resolves to loopback, 0/8, 10/8, 127/8, 169.254/16, " +
|
|
5040
|
+
"172.16-31, 192.168/16, 100.64/10 or fdaa::/16 is REJECTED at write time and again " +
|
|
5041
|
+
"at pin time, whether given literally or via a public hostname that resolves there; " +
|
|
5042
|
+
"private-range carve-outs are available ONLY through the operator-controlled " +
|
|
5043
|
+
"deployment allowlist, which no role can write. (2) PURPOSE-SCOPED — a host declared " +
|
|
5044
|
+
"for inference does NOT authorize webhook POSTs of your content to it, nor loopctl " +
|
|
5045
|
+
"FETCHING tenant-supplied URLs from it (purpose 'ingest'). (3) VENDOR " +
|
|
5046
|
+
"HOSTS EXCLUDED (api.openai.com, api.anthropic.com). Requires your EXACT user-role " +
|
|
5047
|
+
"key.",
|
|
5048
|
+
inputSchema: {
|
|
5049
|
+
type: "object",
|
|
5050
|
+
properties: {
|
|
5051
|
+
host: {
|
|
5052
|
+
type: "string",
|
|
5053
|
+
description:
|
|
5054
|
+
"The PUBLIC hostname (or a full URL — only the authority is kept), e.g. " +
|
|
5055
|
+
"'ollama.example.com'.",
|
|
5056
|
+
},
|
|
5057
|
+
purposes: {
|
|
5058
|
+
type: "array",
|
|
5059
|
+
items: { type: "string", enum: ["inference", "webhook", "ingest"] },
|
|
5060
|
+
description:
|
|
5061
|
+
"What this declaration authorizes. At least one. Declarations are honoured " +
|
|
5062
|
+
"ONLY for their declared purposes.",
|
|
5063
|
+
},
|
|
5064
|
+
note: { type: "string", description: "Free-form operator note." },
|
|
5065
|
+
},
|
|
5066
|
+
required: ["host", "purposes"],
|
|
5067
|
+
},
|
|
5068
|
+
},
|
|
5069
|
+
{
|
|
5070
|
+
name: "revoke_trusted_endpoint",
|
|
5071
|
+
description:
|
|
5072
|
+
"Revoke a tenant-declared trusted endpoint. Invalidation is IMMEDIATE — the " +
|
|
5073
|
+
"declaration does not keep working for the remainder of the pin TTL. Requires your " +
|
|
5074
|
+
"EXACT user-role key.",
|
|
5075
|
+
inputSchema: {
|
|
5076
|
+
type: "object",
|
|
5077
|
+
properties: { host: { type: "string", description: "The declared host to revoke." } },
|
|
5078
|
+
required: ["host"],
|
|
5079
|
+
},
|
|
5080
|
+
},
|
|
5081
|
+
{
|
|
5082
|
+
name: "egress_repin",
|
|
5083
|
+
description:
|
|
5084
|
+
"Recover from a :pin_stale error. loopctl pins the IP set it classified so the " +
|
|
5085
|
+
"address it connects to is the address it vetted (closing DNS rebinding). When your " +
|
|
5086
|
+
"box gets a new lease and the address set changes, calls return the DISTINCT " +
|
|
5087
|
+
":pin_stale error — NOT egress_blocked — and this tool re-resolves and re-pins the " +
|
|
5088
|
+
"host. Available at AGENT role BY DESIGN: home Ollama boxes, tailscale funnels and " +
|
|
5089
|
+
"DHCP VPSes change IP routinely, and requiring a human user-role write to recover " +
|
|
5090
|
+
"would contradict loopctl's agent-native, no-UI design.",
|
|
5091
|
+
inputSchema: {
|
|
5092
|
+
type: "object",
|
|
5093
|
+
properties: {
|
|
5094
|
+
host: { type: "string", description: "The host to re-pin." },
|
|
5095
|
+
project_id: { type: "string", description: "Scope the re-pin to a project." },
|
|
5096
|
+
},
|
|
5097
|
+
required: ["host"],
|
|
5098
|
+
},
|
|
5099
|
+
},
|
|
4866
5100
|
{
|
|
4867
5101
|
name: "llm_config",
|
|
4868
5102
|
description:
|
|
@@ -4893,7 +5127,11 @@ const TOOLS = [
|
|
|
4893
5127
|
"(extraction_model / classification_model / merge_model / embedding_model) are " +
|
|
4894
5128
|
"free-form (any model the key permits) and default server-side when omitted. Typical " +
|
|
4895
5129
|
'onboarding call: set_llm_config({api_key: "sk-ant-...", embedding_api_key: "sk-..."}). ' +
|
|
4896
|
-
"
|
|
5130
|
+
"PRIVATE TIER: set chat_provider 'openai_compatible' + chat_base_url + " +
|
|
5131
|
+
"extraction_model (+ chat_api_key unless your server is keyless) to run " +
|
|
5132
|
+
"extraction/classification/merge against YOUR OWN endpoint so document text " +
|
|
5133
|
+
"never leaves your boundary; every model it resolves to is probed before it is saved. " +
|
|
5134
|
+
"Verify anytime with llm_config (has_api_key / has_embedding_key / chat_provider). REQUIRES your " +
|
|
4897
5135
|
"user-role key LOOPCTL_USER_KEY (minted by the human-anchored signup ceremony); if it " +
|
|
4898
5136
|
"is unset the tool fails fast telling you to set it.",
|
|
4899
5137
|
inputSchema: {
|
|
@@ -4907,7 +5145,14 @@ const TOOLS = [
|
|
|
4907
5145
|
},
|
|
4908
5146
|
extraction_model: {
|
|
4909
5147
|
type: "string",
|
|
4910
|
-
description:
|
|
5148
|
+
description:
|
|
5149
|
+
"Model id for knowledge extraction (null → server default). REQUIRED with " +
|
|
5150
|
+
"chat_provider 'openai_compatible': the server default is an Anthropic model " +
|
|
5151
|
+
"id your endpoint cannot serve, so there is no safe fallback. It is also the " +
|
|
5152
|
+
"fallback for classification_model / merge_model on that provider. " +
|
|
5153
|
+
"HuggingFace repo ids are valid (e.g. 'meta-llama/Meta-Llama-3-8B-Instruct') " +
|
|
5154
|
+
"— it is sent verbatim as the OpenAI `model` field, so it must match the name " +
|
|
5155
|
+
"your server actually serves.",
|
|
4911
5156
|
},
|
|
4912
5157
|
classification_model: {
|
|
4913
5158
|
type: "string",
|
|
@@ -4929,6 +5174,48 @@ const TOOLS = [
|
|
|
4929
5174
|
description:
|
|
4930
5175
|
"Embedding model id (null → server default text-embedding-3-small).",
|
|
4931
5176
|
},
|
|
5177
|
+
chat_provider: {
|
|
5178
|
+
type: "string",
|
|
5179
|
+
enum: ["anthropic", "openai_compatible"],
|
|
5180
|
+
description:
|
|
5181
|
+
"Which provider serves the CHAT surface (ingest extraction, classification, " +
|
|
5182
|
+
"merge synthesis, memory promotion). Omit or 'anthropic' keeps the hardcoded " +
|
|
5183
|
+
"Anthropic endpoint and identical behaviour. 'openai_compatible' routes that " +
|
|
5184
|
+
"surface — the largest and most sensitive payload in the pipeline, your " +
|
|
5185
|
+
"documents' full text — to YOUR OWN endpoint instead.",
|
|
5186
|
+
},
|
|
5187
|
+
chat_base_url: {
|
|
5188
|
+
type: "string",
|
|
5189
|
+
description:
|
|
5190
|
+
"API base of your OpenAI-compatible server, e.g. " +
|
|
5191
|
+
"'https://llm.example.internal/v1' (the client appends /chat/completions). " +
|
|
5192
|
+
"Required with chat_provider 'openai_compatible'. PROBED with a trivial " +
|
|
5193
|
+
"completion per resolved model BEFORE it is saved: an unreachable host, a " +
|
|
5194
|
+
"rejected credential or a non-OpenAI-compatible response is a 422 and NOTHING " +
|
|
5195
|
+
"is persisted. A host resolving into a private/loopback/link-local range is " +
|
|
5196
|
+
"refused outright unless the OPERATOR allowlisted it (SSRF guard). Must be a " +
|
|
5197
|
+
"BARE base: no query string, no fragment, no user:pass@ credentials (this " +
|
|
5198
|
+
"column is NOT encrypted — anything in the URL is stored and echoed back). " +
|
|
5199
|
+
"Plaintext http is accepted ONLY for a host the egress policy classifies " +
|
|
5200
|
+
"network-local; a public http:// endpoint is refused because the request " +
|
|
5201
|
+
"carries your key and your documents' full text in cleartext.",
|
|
5202
|
+
},
|
|
5203
|
+
chat_api_key: {
|
|
5204
|
+
type: "string",
|
|
5205
|
+
description:
|
|
5206
|
+
"Credential for chat_base_url. Write-only; stored encrypted, never returned. " +
|
|
5207
|
+
"SEPARATE from api_key — your Anthropic key is never sent to your endpoint. " +
|
|
5208
|
+
"OPTIONAL: a local server that serves /chat/completions with no auth is " +
|
|
5209
|
+
"configured by omitting this, and no authorization header is then sent.",
|
|
5210
|
+
},
|
|
5211
|
+
acknowledge_key_transmission: {
|
|
5212
|
+
type: "boolean",
|
|
5213
|
+
description:
|
|
5214
|
+
"Required when CHANGING chat_base_url without supplying a matching " +
|
|
5215
|
+
"chat_api_key: explicitly acknowledges that the already-stored key will be " +
|
|
5216
|
+
"transmitted to the new host. The probe never ships an existing credential to " +
|
|
5217
|
+
"a new host silently. Not persisted.",
|
|
5218
|
+
},
|
|
4932
5219
|
},
|
|
4933
5220
|
required: [],
|
|
4934
5221
|
},
|
|
@@ -4936,7 +5223,7 @@ const TOOLS = [
|
|
|
4936
5223
|
{
|
|
4937
5224
|
name: "knowledge_llm_usage",
|
|
4938
5225
|
description:
|
|
4939
|
-
"Per-tenant LLM token-usage summary, grouped by operation + model + source_type + " +
|
|
5226
|
+
"Per-tenant LLM token-usage summary, grouped by operation + model + provider + source_type + " +
|
|
4940
5227
|
"day over an optional date range, newest day first, with offset/limit pagination " +
|
|
4941
5228
|
"over meta.total_count. When `from` is omitted it defaults to a 90-day lookback; the " +
|
|
4942
5229
|
"EFFECTIVE window is echoed in meta.from/meta.to so you can detect that older usage " +
|
|
@@ -5684,6 +5971,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5684
5971
|
case "knowledge_ingestion_jobs":
|
|
5685
5972
|
return await knowledgeIngestionJobs(args);
|
|
5686
5973
|
|
|
5974
|
+
// US-41.4 — fail-closed no-egress guard
|
|
5975
|
+
case "egress_posture":
|
|
5976
|
+
return await egressPosture();
|
|
5977
|
+
|
|
5978
|
+
case "set_local_only":
|
|
5979
|
+
return await setLocalOnly(args);
|
|
5980
|
+
|
|
5981
|
+
case "clear_local_only":
|
|
5982
|
+
return await clearLocalOnly(args);
|
|
5983
|
+
|
|
5984
|
+
case "declare_trusted_endpoint":
|
|
5985
|
+
return await declareTrustedEndpoint(args);
|
|
5986
|
+
|
|
5987
|
+
case "revoke_trusted_endpoint":
|
|
5988
|
+
return await revokeTrustedEndpoint(args);
|
|
5989
|
+
|
|
5990
|
+
case "egress_repin":
|
|
5991
|
+
return await egressRepin(args);
|
|
5992
|
+
|
|
5687
5993
|
// Per-tenant BYO LLM config + usage (Epic 28, #179)
|
|
5688
5994
|
case "llm_config":
|
|
5689
5995
|
return await llmConfig();
|