loopctl-mcp-server 2.52.0 → 2.53.1
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 +15 -1
- package/index.js +240 -0
- 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
|
|
@@ -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
|
@@ -1871,6 +1871,88 @@ async function setLlmConfig({
|
|
|
1871
1871
|
return toContent(result);
|
|
1872
1872
|
}
|
|
1873
1873
|
|
|
1874
|
+
// --- US-41.4: fail-closed no-egress guard -----------------------------------
|
|
1875
|
+
//
|
|
1876
|
+
// egress_posture is a READ available at AGENT role: an agent must be able to
|
|
1877
|
+
// verify locality with the key it already has, or the whole verify-before-harvest
|
|
1878
|
+
// workflow fails. WRITING local_only is asymmetric: ENABLE (tightening) is
|
|
1879
|
+
// orchestrator+, CLEAR (widening) is user-only, and declaring a trusted endpoint
|
|
1880
|
+
// is user-only. Re-pinning is agent-role on purpose — a home Ollama box or a DHCP
|
|
1881
|
+
// VPS changes IP routinely and recovery must not need a human.
|
|
1882
|
+
async function egressPosture() {
|
|
1883
|
+
const result = await apiCall(
|
|
1884
|
+
"GET",
|
|
1885
|
+
"/api/v1/egress/posture",
|
|
1886
|
+
null,
|
|
1887
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1888
|
+
);
|
|
1889
|
+
return toContent(result);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
async function setLocalOnly({ project_id, acknowledge } = {}) {
|
|
1893
|
+
const body = {};
|
|
1894
|
+
if (project_id != null) body.project_id = project_id;
|
|
1895
|
+
if (acknowledge === true) body.acknowledge = true;
|
|
1896
|
+
// ENABLE = tightening = orchestrator+.
|
|
1897
|
+
const result = await apiCall(
|
|
1898
|
+
"POST",
|
|
1899
|
+
"/api/v1/egress/local-only",
|
|
1900
|
+
body,
|
|
1901
|
+
process.env.LOOPCTL_ORCH_KEY,
|
|
1902
|
+
);
|
|
1903
|
+
return toContent(result);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
async function clearLocalOnly({ project_id } = {}) {
|
|
1907
|
+
// CLEAR = self-widening = EXACT user key. An agent must never be able to
|
|
1908
|
+
// re-open egress one tool call before a harvest.
|
|
1909
|
+
const qs = project_id ? `?project_id=${encodeURIComponent(project_id)}` : "";
|
|
1910
|
+
const result = await apiCall(
|
|
1911
|
+
"DELETE",
|
|
1912
|
+
`/api/v1/egress/local-only${qs}`,
|
|
1913
|
+
null,
|
|
1914
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1915
|
+
{ exactKey: true },
|
|
1916
|
+
);
|
|
1917
|
+
return toContent(result);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
async function declareTrustedEndpoint({ host, purposes, note } = {}) {
|
|
1921
|
+
const body = { host, purposes };
|
|
1922
|
+
if (note != null) body.note = note;
|
|
1923
|
+
const result = await apiCall(
|
|
1924
|
+
"POST",
|
|
1925
|
+
"/api/v1/egress/trusted-endpoints",
|
|
1926
|
+
body,
|
|
1927
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1928
|
+
{ exactKey: true },
|
|
1929
|
+
);
|
|
1930
|
+
return toContent(result);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
async function revokeTrustedEndpoint({ host } = {}) {
|
|
1934
|
+
const result = await apiCall(
|
|
1935
|
+
"DELETE",
|
|
1936
|
+
`/api/v1/egress/trusted-endpoints/${encodeURIComponent(host)}`,
|
|
1937
|
+
null,
|
|
1938
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1939
|
+
{ exactKey: true },
|
|
1940
|
+
);
|
|
1941
|
+
return toContent(result);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async function egressRepin({ host, project_id } = {}) {
|
|
1945
|
+
const body = { host };
|
|
1946
|
+
if (project_id != null) body.project_id = project_id;
|
|
1947
|
+
const result = await apiCall(
|
|
1948
|
+
"POST",
|
|
1949
|
+
"/api/v1/egress/repin",
|
|
1950
|
+
body,
|
|
1951
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1952
|
+
);
|
|
1953
|
+
return toContent(result);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1874
1956
|
async function knowledgeLlmUsage(args = {}) {
|
|
1875
1957
|
// Query-string building lives in lib/http-helpers.js so the test suite exercises
|
|
1876
1958
|
// the same from/to/limit/offset logic the server ships.
|
|
@@ -4863,6 +4945,145 @@ const TOOLS = [
|
|
|
4863
4945
|
},
|
|
4864
4946
|
|
|
4865
4947
|
// Per-tenant BYO LLM config + usage (Epic 28 residual, #179)
|
|
4948
|
+
{
|
|
4949
|
+
name: "egress_posture",
|
|
4950
|
+
description:
|
|
4951
|
+
"VERIFY BEFORE YOU HARVEST. Reports this instance's egress posture for YOUR tenant: " +
|
|
4952
|
+
"the resolved embedding and chat endpoints, a locality VERDICT for each " +
|
|
4953
|
+
"(network-local / 'tenant-declared (unverified attestation), not network-local' / " +
|
|
4954
|
+
"non-local), your declared trusted endpoints with their purposes, per-scope " +
|
|
4955
|
+
"local_only status, and any named posture defects. Endpoints are shown; KEYS NEVER " +
|
|
4956
|
+
"ARE. Call this BEFORE sending private documents anywhere, instead of trusting that " +
|
|
4957
|
+
"the operator configured things correctly. READ tool, available at AGENT role — the " +
|
|
4958
|
+
"key you already have. NOTE the deployment allowlist CONTENTS are operator " +
|
|
4959
|
+
"infrastructure and are NOT disclosed at agent role: you get only a boolean per " +
|
|
4960
|
+
"endpoint saying whether its verdict came from the allowlist (contents at user+). " +
|
|
4961
|
+
"SCOPE OF THE GUARANTEE: fail-closed enforcement covers every outbound HTTP call " +
|
|
4962
|
+
"made by loopctl application code on the MODEL-PROVIDER path. Webhook delivery is " +
|
|
4963
|
+
"not covered yet (US-41.5), and HTTP performed inside a dependency, plus this " +
|
|
4964
|
+
"separate mcp-server codebase, are outside the static chokepoint check.",
|
|
4965
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
4966
|
+
},
|
|
4967
|
+
{
|
|
4968
|
+
name: "set_local_only",
|
|
4969
|
+
description:
|
|
4970
|
+
"TIGHTEN the posture: mark a scope local_only so loopctl HARD-REFUSES any " +
|
|
4971
|
+
"model-provider call whose resolved endpoint is not classified local. Default is OFF " +
|
|
4972
|
+
"everywhere; nothing changes until a scope opts in. Scope resolution is " +
|
|
4973
|
+
"MOST-RESTRICTIVE-WINS (project OR tenant) and a project can NEVER relax a tenant " +
|
|
4974
|
+
"marking. MANDATORY PRE-FLIGHT: the call is REFUSED with 409 would_block_endpoints, " +
|
|
4975
|
+
"naming every endpoint that would become egress_blocked, unless you pass " +
|
|
4976
|
+
"acknowledge: true — because on a tenant still using vendor default endpoints this " +
|
|
4977
|
+
"instantly stops embedding, extraction, classification and merge, and only a " +
|
|
4978
|
+
"human user-role key can undo it. Requires an ORCHESTRATOR key: tightening is safe " +
|
|
4979
|
+
"to automate. CLEARING is a different tool (clear_local_only) and is user-only.",
|
|
4980
|
+
inputSchema: {
|
|
4981
|
+
type: "object",
|
|
4982
|
+
properties: {
|
|
4983
|
+
project_id: {
|
|
4984
|
+
type: "string",
|
|
4985
|
+
description:
|
|
4986
|
+
"Mark this PROJECT only (omit to mark the whole tenant). A project-less row — " +
|
|
4987
|
+
"a tenant-wide article, any memory — always follows the TENANT marking.",
|
|
4988
|
+
},
|
|
4989
|
+
acknowledge: {
|
|
4990
|
+
type: "boolean",
|
|
4991
|
+
description:
|
|
4992
|
+
"Accept the reported blocked posture and proceed. Required when the pre-flight " +
|
|
4993
|
+
"finds endpoints that would become egress_blocked.",
|
|
4994
|
+
},
|
|
4995
|
+
},
|
|
4996
|
+
required: [],
|
|
4997
|
+
},
|
|
4998
|
+
},
|
|
4999
|
+
{
|
|
5000
|
+
name: "clear_local_only",
|
|
5001
|
+
description:
|
|
5002
|
+
"WIDEN the posture: remove a scope's local_only marking, re-permitting non-local " +
|
|
5003
|
+
"model-provider egress for it. Requires your EXACT user-role key " +
|
|
5004
|
+
"(LOOPCTL_USER_KEY) — deliberately NOT available to an agent or an orchestrator, " +
|
|
5005
|
+
"because clearing is the self-widening move that would otherwise let an automated " +
|
|
5006
|
+
"key re-open egress immediately before a harvest. Audited with actor and scope.",
|
|
5007
|
+
inputSchema: {
|
|
5008
|
+
type: "object",
|
|
5009
|
+
properties: {
|
|
5010
|
+
project_id: {
|
|
5011
|
+
type: "string",
|
|
5012
|
+
description: "Clear this PROJECT's marking (omit to clear the tenant marking).",
|
|
5013
|
+
},
|
|
5014
|
+
},
|
|
5015
|
+
required: [],
|
|
5016
|
+
},
|
|
5017
|
+
},
|
|
5018
|
+
{
|
|
5019
|
+
name: "declare_trusted_endpoint",
|
|
5020
|
+
description:
|
|
5021
|
+
"Declare a host you attest is YOUR OWN (your VPS, tailscale funnel, or domain) so a " +
|
|
5022
|
+
"local_only scope can reach it. THIS IS AN UNVERIFIED TENANT ATTESTATION, NOT " +
|
|
5023
|
+
"NETWORK LOCALITY: loopctl does not prove you own the host, and the posture report " +
|
|
5024
|
+
"and custody claim label it 'tenant-declared (unverified attestation), not " +
|
|
5025
|
+
"network-local' — never as network-local. THREE ENFORCED CONSTRAINTS: (1) PUBLIC " +
|
|
5026
|
+
"ADDRESSES ONLY — a host that resolves to loopback, 0/8, 10/8, 127/8, 169.254/16, " +
|
|
5027
|
+
"172.16-31, 192.168/16, 100.64/10 or fdaa::/16 is REJECTED at write time and again " +
|
|
5028
|
+
"at pin time, whether given literally or via a public hostname that resolves there; " +
|
|
5029
|
+
"private-range carve-outs are available ONLY through the operator-controlled " +
|
|
5030
|
+
"deployment allowlist, which no role can write. (2) PURPOSE-SCOPED — a host declared " +
|
|
5031
|
+
"for inference does NOT authorize webhook POSTs of your content to it, nor loopctl " +
|
|
5032
|
+
"FETCHING tenant-supplied URLs from it (purpose 'ingest'). (3) VENDOR " +
|
|
5033
|
+
"HOSTS EXCLUDED (api.openai.com, api.anthropic.com). Requires your EXACT user-role " +
|
|
5034
|
+
"key.",
|
|
5035
|
+
inputSchema: {
|
|
5036
|
+
type: "object",
|
|
5037
|
+
properties: {
|
|
5038
|
+
host: {
|
|
5039
|
+
type: "string",
|
|
5040
|
+
description:
|
|
5041
|
+
"The PUBLIC hostname (or a full URL — only the authority is kept), e.g. " +
|
|
5042
|
+
"'ollama.example.com'.",
|
|
5043
|
+
},
|
|
5044
|
+
purposes: {
|
|
5045
|
+
type: "array",
|
|
5046
|
+
items: { type: "string", enum: ["inference", "webhook", "ingest"] },
|
|
5047
|
+
description:
|
|
5048
|
+
"What this declaration authorizes. At least one. Declarations are honoured " +
|
|
5049
|
+
"ONLY for their declared purposes.",
|
|
5050
|
+
},
|
|
5051
|
+
note: { type: "string", description: "Free-form operator note." },
|
|
5052
|
+
},
|
|
5053
|
+
required: ["host", "purposes"],
|
|
5054
|
+
},
|
|
5055
|
+
},
|
|
5056
|
+
{
|
|
5057
|
+
name: "revoke_trusted_endpoint",
|
|
5058
|
+
description:
|
|
5059
|
+
"Revoke a tenant-declared trusted endpoint. Invalidation is IMMEDIATE — the " +
|
|
5060
|
+
"declaration does not keep working for the remainder of the pin TTL. Requires your " +
|
|
5061
|
+
"EXACT user-role key.",
|
|
5062
|
+
inputSchema: {
|
|
5063
|
+
type: "object",
|
|
5064
|
+
properties: { host: { type: "string", description: "The declared host to revoke." } },
|
|
5065
|
+
required: ["host"],
|
|
5066
|
+
},
|
|
5067
|
+
},
|
|
5068
|
+
{
|
|
5069
|
+
name: "egress_repin",
|
|
5070
|
+
description:
|
|
5071
|
+
"Recover from a :pin_stale error. loopctl pins the IP set it classified so the " +
|
|
5072
|
+
"address it connects to is the address it vetted (closing DNS rebinding). When your " +
|
|
5073
|
+
"box gets a new lease and the address set changes, calls return the DISTINCT " +
|
|
5074
|
+
":pin_stale error — NOT egress_blocked — and this tool re-resolves and re-pins the " +
|
|
5075
|
+
"host. Available at AGENT role BY DESIGN: home Ollama boxes, tailscale funnels and " +
|
|
5076
|
+
"DHCP VPSes change IP routinely, and requiring a human user-role write to recover " +
|
|
5077
|
+
"would contradict loopctl's agent-native, no-UI design.",
|
|
5078
|
+
inputSchema: {
|
|
5079
|
+
type: "object",
|
|
5080
|
+
properties: {
|
|
5081
|
+
host: { type: "string", description: "The host to re-pin." },
|
|
5082
|
+
project_id: { type: "string", description: "Scope the re-pin to a project." },
|
|
5083
|
+
},
|
|
5084
|
+
required: ["host"],
|
|
5085
|
+
},
|
|
5086
|
+
},
|
|
4866
5087
|
{
|
|
4867
5088
|
name: "llm_config",
|
|
4868
5089
|
description:
|
|
@@ -5684,6 +5905,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5684
5905
|
case "knowledge_ingestion_jobs":
|
|
5685
5906
|
return await knowledgeIngestionJobs(args);
|
|
5686
5907
|
|
|
5908
|
+
// US-41.4 — fail-closed no-egress guard
|
|
5909
|
+
case "egress_posture":
|
|
5910
|
+
return await egressPosture();
|
|
5911
|
+
|
|
5912
|
+
case "set_local_only":
|
|
5913
|
+
return await setLocalOnly(args);
|
|
5914
|
+
|
|
5915
|
+
case "clear_local_only":
|
|
5916
|
+
return await clearLocalOnly(args);
|
|
5917
|
+
|
|
5918
|
+
case "declare_trusted_endpoint":
|
|
5919
|
+
return await declareTrustedEndpoint(args);
|
|
5920
|
+
|
|
5921
|
+
case "revoke_trusted_endpoint":
|
|
5922
|
+
return await revokeTrustedEndpoint(args);
|
|
5923
|
+
|
|
5924
|
+
case "egress_repin":
|
|
5925
|
+
return await egressRepin(args);
|
|
5926
|
+
|
|
5687
5927
|
// Per-tenant BYO LLM config + usage (Epic 28, #179)
|
|
5688
5928
|
case "llm_config":
|
|
5689
5929
|
return await llmConfig();
|