loopctl-mcp-server 2.35.0 → 2.37.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 +18 -1
- package/index.js +445 -0
- package/lib/http-helpers.js +18 -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 (82)
|
|
125
125
|
|
|
126
126
|
### Project Tools
|
|
127
127
|
|
|
@@ -217,6 +217,23 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
|
|
|
217
217
|
| `knowledge_okf_export` | **Requires `LOOPCTL_USER_KEY`.** Export the wiki as a portable OKF (Open Knowledge Format) v0.1 bundle of markdown files. Writes to `out_dir`, or returns `{files, meta}` inline. |
|
|
218
218
|
| `knowledge_okf_import` | **Requires `LOOPCTL_USER_KEY`.** Import an OKF v0.1 bundle from a local directory. Creates or (with `merge`) updates articles; tolerates and preserves unknown frontmatter. |
|
|
219
219
|
|
|
220
|
+
### Agent Memory Tools (agent key)
|
|
221
|
+
|
|
222
|
+
`memory_*` is YOUR OWN scoped, private, accumulated working state — running notes, in-flight
|
|
223
|
+
task context, recall across sessions — NOT the shared knowledge wiki. Use `memory_*` for that
|
|
224
|
+
private per-scope state; use `knowledge_*` for curated knowledge other agents should see. Scope
|
|
225
|
+
(`tenant_id`/`subject_id`) is resolved server-side from your API key — none of these tools accept
|
|
226
|
+
a `tenant_id`/`subject_id`, so there is no NON-SUPERADMIN way to read or write into another
|
|
227
|
+
scope. (The one carve-out: `memory_list`'s `all_subjects` boolean IS a cross-subject read, but
|
|
228
|
+
it is enforced server-side and a no-op for a non-superadmin key — see below.)
|
|
229
|
+
|
|
230
|
+
| Tool | Description |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `memory_remember` | Write to your own working memory. `tier` selects the substrate: `long_term` (default; requires `text`, embedded asynchronously and later recalled by semantic similarity via `memory_recall`) or `session` (short-term; requires `session_id`, `content`, `expires_at` — pruned after expiry, not semantically recalled). Returns 201 with the stored memory. Optional: `confidence`, `tags`, `source_session_id`, `metadata` (long-term); `role` (session). |
|
|
233
|
+
| `memory_recall` | Semantically recall your own long-term memories most similar to `query`. When embedding generation is unavailable the response degrades to a recent-first text match with `meta.fallback: true` and a stable `meta.reason` (score is `null` on that path) — check `meta.fallback` before treating a short/empty result as a genuinely empty scope. `meta.total_count`/`meta.underfilled` are also returned. Optional: `limit`, `include_superseded`. |
|
|
234
|
+
| `memory_list` | List your own long-term memories, newest first, paginated with `meta.total_count/limit/offset` (the true scoped count, never silently capped by `limit`). Optional: `limit`, `offset`, `include_superseded`, `all_subjects` (superadmin only; ignored for non-superadmin keys). |
|
|
235
|
+
| `memory_forget` | Delete one of your own long-term memories by id. A foreign-subject, foreign-tenant, or unknown id returns 404 (no existence leak). Required: `id`. |
|
|
236
|
+
|
|
220
237
|
### Knowledge Management Tools (orchestrator key)
|
|
221
238
|
|
|
222
239
|
| Tool | Description |
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
projectsPath,
|
|
19
19
|
ingestionJobsPath,
|
|
20
20
|
llmUsagePath,
|
|
21
|
+
memoryPath,
|
|
21
22
|
parseJsonResponseBody,
|
|
22
23
|
} from "./lib/http-helpers.js";
|
|
23
24
|
import {
|
|
@@ -1064,6 +1065,100 @@ async function knowledgeCreate({
|
|
|
1064
1065
|
return toContent(result);
|
|
1065
1066
|
}
|
|
1066
1067
|
|
|
1068
|
+
// --- Agent Memory Tools (US-28.4) ---
|
|
1069
|
+
//
|
|
1070
|
+
// memory_* is YOUR own scoped, private, accumulated working state — recall
|
|
1071
|
+
// across sessions, running notes, in-flight task context. knowledge_* is the
|
|
1072
|
+
// curated, shared wiki. Scope (tenant_id/subject_id) is resolved SERVER-SIDE
|
|
1073
|
+
// from the API key (US-28.3) — these tools never accept or forward a
|
|
1074
|
+
// tenant_id/subject_id, so there is no NON-SUPERADMIN way to express a
|
|
1075
|
+
// cross-scope read/write here even by mistake. (The one carve-out:
|
|
1076
|
+
// memory_list's `all_subjects` boolean IS a cross-subject read, but it is
|
|
1077
|
+
// enforced server-side and a no-op for a non-superadmin key — see its
|
|
1078
|
+
// description below.) Routes through the shared apiCall/witness client
|
|
1079
|
+
// (same as every other write tool), so witness/STH persistence + the
|
|
1080
|
+
// transparent 412 self-heal on a fresh MCP process apply automatically
|
|
1081
|
+
// (AC-28.4.3) — no bespoke witness code needed.
|
|
1082
|
+
|
|
1083
|
+
async function memoryRemember({
|
|
1084
|
+
tier,
|
|
1085
|
+
text,
|
|
1086
|
+
confidence,
|
|
1087
|
+
tags,
|
|
1088
|
+
source_session_id,
|
|
1089
|
+
session_id,
|
|
1090
|
+
role,
|
|
1091
|
+
content,
|
|
1092
|
+
expires_at,
|
|
1093
|
+
metadata,
|
|
1094
|
+
}) {
|
|
1095
|
+
const payload = {};
|
|
1096
|
+
if (tier) payload.tier = tier;
|
|
1097
|
+
if (text != null) payload.text = text;
|
|
1098
|
+
if (confidence != null) payload.confidence = confidence;
|
|
1099
|
+
if (tags) payload.tags = tags;
|
|
1100
|
+
if (source_session_id) payload.source_session_id = source_session_id;
|
|
1101
|
+
if (session_id) payload.session_id = session_id;
|
|
1102
|
+
if (role) payload.role = role;
|
|
1103
|
+
if (content != null) payload.content = content;
|
|
1104
|
+
if (expires_at) payload.expires_at = expires_at;
|
|
1105
|
+
if (metadata != null) payload.metadata = metadata;
|
|
1106
|
+
|
|
1107
|
+
const result = await apiCall(
|
|
1108
|
+
"POST",
|
|
1109
|
+
"/api/v1/memory",
|
|
1110
|
+
payload,
|
|
1111
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1112
|
+
);
|
|
1113
|
+
return toContent(result);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async function memoryRecall({ query, limit, include_superseded }) {
|
|
1117
|
+
const payload = { query };
|
|
1118
|
+
if (limit != null) payload.limit = limit;
|
|
1119
|
+
if (include_superseded != null) payload.include_superseded = include_superseded;
|
|
1120
|
+
|
|
1121
|
+
const result = await apiCall(
|
|
1122
|
+
"POST",
|
|
1123
|
+
"/api/v1/memory/recall",
|
|
1124
|
+
payload,
|
|
1125
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1126
|
+
);
|
|
1127
|
+
// Surface meta (fallback/reason/total_count/underfilled) so the caller can tell
|
|
1128
|
+
// a degraded recall from a genuinely empty scope (AC-28.4.4) — toContent already
|
|
1129
|
+
// preserves the full result (data + meta), we just keep this call explicit.
|
|
1130
|
+
return toContent(result);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function memoryList({ limit, offset, include_superseded, all_subjects }) {
|
|
1134
|
+
// all_subjects is superadmin-only server-side; a non-superadmin key sending
|
|
1135
|
+
// this is ignored (falls back to its own subject) rather than erroring — the
|
|
1136
|
+
// one deliberate cross-subject read this MCP surface can express (module
|
|
1137
|
+
// comment above), and only for a superadmin caller.
|
|
1138
|
+
const path = memoryPath({ limit, offset, include_superseded, all_subjects });
|
|
1139
|
+
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
1140
|
+
// Surface meta.total_count/limit/offset (AC-28.4.4).
|
|
1141
|
+
return toContent(result);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function memoryForget({ id }) {
|
|
1145
|
+
// Path-injection guard (mirrors knowledgeAgentUsage's UUID_RE check below).
|
|
1146
|
+
if (typeof id !== "string" || !UUID_RE.test(id)) {
|
|
1147
|
+
return {
|
|
1148
|
+
content: [{ type: "text", text: "Error: id must be a canonical UUID (8-4-4-4-12 hex)." }],
|
|
1149
|
+
isError: true,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const result = await apiCall(
|
|
1154
|
+
"DELETE",
|
|
1155
|
+
`/api/v1/memory/${id}`,
|
|
1156
|
+
null,
|
|
1157
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1158
|
+
);
|
|
1159
|
+
return toContent(result);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1067
1162
|
// --- Knowledge Management Tools (orch key) ---
|
|
1068
1163
|
|
|
1069
1164
|
async function knowledgePublish({ article_id }) {
|
|
@@ -1690,6 +1785,71 @@ async function signup({ name, slug, email }) {
|
|
|
1690
1785
|
return toContent(result);
|
|
1691
1786
|
}
|
|
1692
1787
|
|
|
1788
|
+
// US-26.7.2: opt-in WebAuthn trust-tier upgrade ceremony (agent_rooted ->
|
|
1789
|
+
// human_anchored) + authenticator revocation. All four require the EXACT
|
|
1790
|
+
// user-role key (LOOPCTL_USER_KEY) + ownership of `tenant_id` — mirroring
|
|
1791
|
+
// set_llm_config's exactKey:true pattern, since these mutate the tenant's
|
|
1792
|
+
// root of trust. NOT headless: completing enrollment/revocation requires an
|
|
1793
|
+
// INTERACTIVE WebAuthn client (a browser or a native FIDO2 library) with a
|
|
1794
|
+
// human present to touch the hardware authenticator — an agent alone cannot
|
|
1795
|
+
// produce a valid attestation or assertion, by design (see
|
|
1796
|
+
// docs/chain-of-custody-v2.md §9).
|
|
1797
|
+
async function requestAuthenticatorChallenge({ tenant_id }) {
|
|
1798
|
+
const result = await apiCall(
|
|
1799
|
+
"POST",
|
|
1800
|
+
`/api/v1/tenants/${tenant_id}/authenticators/challenge`,
|
|
1801
|
+
{},
|
|
1802
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1803
|
+
{ exactKey: true },
|
|
1804
|
+
);
|
|
1805
|
+
return toContent(result);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async function enrollAuthenticator({
|
|
1809
|
+
tenant_id,
|
|
1810
|
+
challenge_id,
|
|
1811
|
+
attestation_object,
|
|
1812
|
+
client_data_json,
|
|
1813
|
+
credential_id,
|
|
1814
|
+
friendly_name,
|
|
1815
|
+
reauth_assertion,
|
|
1816
|
+
}) {
|
|
1817
|
+
const body = { challenge_id, attestation_object, client_data_json, credential_id };
|
|
1818
|
+
if (friendly_name != null) body.friendly_name = friendly_name;
|
|
1819
|
+
if (reauth_assertion != null) body.reauth_assertion = reauth_assertion;
|
|
1820
|
+
|
|
1821
|
+
const result = await apiCall(
|
|
1822
|
+
"POST",
|
|
1823
|
+
`/api/v1/tenants/${tenant_id}/authenticators`,
|
|
1824
|
+
body,
|
|
1825
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1826
|
+
{ exactKey: true },
|
|
1827
|
+
);
|
|
1828
|
+
return toContent(result);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
async function requestAuthenticatorRevokeChallenge({ tenant_id }) {
|
|
1832
|
+
const result = await apiCall(
|
|
1833
|
+
"POST",
|
|
1834
|
+
`/api/v1/tenants/${tenant_id}/authenticators/revoke-challenge`,
|
|
1835
|
+
{},
|
|
1836
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1837
|
+
{ exactKey: true },
|
|
1838
|
+
);
|
|
1839
|
+
return toContent(result);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
async function revokeAuthenticator({ tenant_id, authenticator_id, webauthn_assertion }) {
|
|
1843
|
+
const result = await apiCall(
|
|
1844
|
+
"DELETE",
|
|
1845
|
+
`/api/v1/tenants/${tenant_id}/authenticators/${authenticator_id}`,
|
|
1846
|
+
{ webauthn_assertion },
|
|
1847
|
+
process.env.LOOPCTL_USER_KEY,
|
|
1848
|
+
{ exactKey: true },
|
|
1849
|
+
);
|
|
1850
|
+
return toContent(result);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1693
1853
|
// US-26: Signed Tree Head retrieval
|
|
1694
1854
|
async function getSth({ tenant_id }) {
|
|
1695
1855
|
const result = await apiCall("GET", `/api/v1/audit/sth/${tenant_id}`);
|
|
@@ -2987,6 +3147,158 @@ const TOOLS = [
|
|
|
2987
3147
|
},
|
|
2988
3148
|
},
|
|
2989
3149
|
|
|
3150
|
+
// Agent Memory Tools (US-28.4)
|
|
3151
|
+
{
|
|
3152
|
+
name: "memory_remember",
|
|
3153
|
+
description:
|
|
3154
|
+
"Write to YOUR OWN scoped, private, accumulated working memory — running notes, " +
|
|
3155
|
+
"in-flight task state, decisions you made this session — NOT the shared knowledge " +
|
|
3156
|
+
"wiki. Use memory_* for private per-scope working state across sessions; use " +
|
|
3157
|
+
"knowledge_* for curated, shared knowledge articles other agents should see. Scope " +
|
|
3158
|
+
"(tenant_id/subject_id) is resolved server-side from your API key — never pass or " +
|
|
3159
|
+
"expect a tenant_id/subject_id here; there is no way to write into another scope. " +
|
|
3160
|
+
"`tier` selects the substrate: `long_term` (default; requires `text`, embedded " +
|
|
3161
|
+
"asynchronously and later recalled by semantic similarity via memory_recall) or " +
|
|
3162
|
+
"`session` (short-term; requires `session_id`, `content`, `expires_at` — pruned " +
|
|
3163
|
+
"after expiry, not semantically recalled). Returns 201 with the stored memory.",
|
|
3164
|
+
inputSchema: {
|
|
3165
|
+
type: "object",
|
|
3166
|
+
properties: {
|
|
3167
|
+
tier: {
|
|
3168
|
+
type: "string",
|
|
3169
|
+
enum: ["long_term", "session"],
|
|
3170
|
+
description: "Memory substrate. Defaults to long_term.",
|
|
3171
|
+
},
|
|
3172
|
+
text: {
|
|
3173
|
+
type: "string",
|
|
3174
|
+
description: "Long-term memory content (required when tier=long_term).",
|
|
3175
|
+
},
|
|
3176
|
+
confidence: {
|
|
3177
|
+
type: "number",
|
|
3178
|
+
description: "Optional: confidence score (0.0-1.0) for a long-term memory.",
|
|
3179
|
+
},
|
|
3180
|
+
tags: {
|
|
3181
|
+
type: "array",
|
|
3182
|
+
items: { type: "string" },
|
|
3183
|
+
description: "Optional: tags for a long-term memory.",
|
|
3184
|
+
},
|
|
3185
|
+
source_session_id: {
|
|
3186
|
+
type: "string",
|
|
3187
|
+
description: "Optional: the session this long-term memory was distilled from.",
|
|
3188
|
+
},
|
|
3189
|
+
session_id: {
|
|
3190
|
+
type: "string",
|
|
3191
|
+
description: "Session identifier (required when tier=session).",
|
|
3192
|
+
},
|
|
3193
|
+
role: {
|
|
3194
|
+
type: "string",
|
|
3195
|
+
enum: ["user", "assistant", "system", "fact"],
|
|
3196
|
+
description: "Optional: speaker role for a session-tier turn.",
|
|
3197
|
+
},
|
|
3198
|
+
content: {
|
|
3199
|
+
type: "string",
|
|
3200
|
+
description: "Session turn content (required when tier=session).",
|
|
3201
|
+
},
|
|
3202
|
+
expires_at: {
|
|
3203
|
+
type: "string",
|
|
3204
|
+
format: "date-time",
|
|
3205
|
+
description: "Prune deadline (required when tier=session).",
|
|
3206
|
+
},
|
|
3207
|
+
metadata: {
|
|
3208
|
+
type: "object",
|
|
3209
|
+
description: "Optional: arbitrary structured metadata to attach to the memory.",
|
|
3210
|
+
},
|
|
3211
|
+
},
|
|
3212
|
+
required: [],
|
|
3213
|
+
},
|
|
3214
|
+
},
|
|
3215
|
+
{
|
|
3216
|
+
name: "memory_recall",
|
|
3217
|
+
description:
|
|
3218
|
+
"Semantically recall YOUR OWN long-term memories most similar to `query` — private, " +
|
|
3219
|
+
"scoped working state, NOT the shared knowledge wiki. Use memory_* for your scoped, " +
|
|
3220
|
+
"private, accumulated working state across sessions; use knowledge_* for curated, " +
|
|
3221
|
+
"shared knowledge articles. Scope is resolved server-side from your API key. When " +
|
|
3222
|
+
"embedding generation is unavailable the response degrades to a recent-first text " +
|
|
3223
|
+
"match with `meta.fallback: true` and a stable `meta.reason` (score is null on that " +
|
|
3224
|
+
"path) — check meta.fallback before treating a short/empty result as a genuinely " +
|
|
3225
|
+
"empty scope. `meta.total_count` and `meta.underfilled` are also returned so you can " +
|
|
3226
|
+
"distinguish a short page from a hard cap.",
|
|
3227
|
+
inputSchema: {
|
|
3228
|
+
type: "object",
|
|
3229
|
+
properties: {
|
|
3230
|
+
query: {
|
|
3231
|
+
type: "string",
|
|
3232
|
+
description: "Text to embed / match against.",
|
|
3233
|
+
},
|
|
3234
|
+
limit: {
|
|
3235
|
+
type: "integer",
|
|
3236
|
+
description: "Optional: max results, clamped to the vector-search max (no silent hard cap).",
|
|
3237
|
+
},
|
|
3238
|
+
include_superseded: {
|
|
3239
|
+
type: "boolean",
|
|
3240
|
+
description: "Optional: include superseded memories (default false).",
|
|
3241
|
+
},
|
|
3242
|
+
},
|
|
3243
|
+
required: ["query"],
|
|
3244
|
+
},
|
|
3245
|
+
},
|
|
3246
|
+
{
|
|
3247
|
+
name: "memory_list",
|
|
3248
|
+
description:
|
|
3249
|
+
"List YOUR OWN long-term memories, newest first — private, scoped working state, " +
|
|
3250
|
+
"NOT the shared knowledge wiki. Use memory_* for your scoped, private, accumulated " +
|
|
3251
|
+
"working state across sessions; use knowledge_* for curated, shared knowledge " +
|
|
3252
|
+
"articles. Scope is resolved server-side from your API key. Paginated with " +
|
|
3253
|
+
"`meta.total_count/limit/offset` (the true scoped count, never silently capped by " +
|
|
3254
|
+
"limit) so you can distinguish an empty scope from a short page.",
|
|
3255
|
+
inputSchema: {
|
|
3256
|
+
type: "object",
|
|
3257
|
+
properties: {
|
|
3258
|
+
limit: {
|
|
3259
|
+
type: "integer",
|
|
3260
|
+
description: "Optional: page size (default 50, max 200).",
|
|
3261
|
+
},
|
|
3262
|
+
offset: {
|
|
3263
|
+
type: "integer",
|
|
3264
|
+
description: "Optional: records to skip (default 0).",
|
|
3265
|
+
},
|
|
3266
|
+
include_superseded: {
|
|
3267
|
+
type: "boolean",
|
|
3268
|
+
description: "Optional: include superseded memories (default false).",
|
|
3269
|
+
},
|
|
3270
|
+
all_subjects: {
|
|
3271
|
+
type: "boolean",
|
|
3272
|
+
description:
|
|
3273
|
+
"Optional, superadmin only: list all subjects' memories in the tenant. Ignored " +
|
|
3274
|
+
"(falls back to your own subject) for non-superadmin keys.",
|
|
3275
|
+
},
|
|
3276
|
+
},
|
|
3277
|
+
required: [],
|
|
3278
|
+
},
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
name: "memory_forget",
|
|
3282
|
+
description:
|
|
3283
|
+
"Delete one of YOUR OWN long-term memories by id — private, scoped working state, " +
|
|
3284
|
+
"NOT the shared knowledge wiki. Use memory_* for your scoped, private, accumulated " +
|
|
3285
|
+
"working state; use knowledge_* for curated, shared knowledge articles (delete those " +
|
|
3286
|
+
"with knowledge_delete). Scope is resolved server-side from your API key: a foreign-" +
|
|
3287
|
+
"subject, foreign-tenant, or unknown id returns 404 (no existence leak) rather than " +
|
|
3288
|
+
"revealing whether it exists in another scope.",
|
|
3289
|
+
inputSchema: {
|
|
3290
|
+
type: "object",
|
|
3291
|
+
properties: {
|
|
3292
|
+
id: {
|
|
3293
|
+
type: "string",
|
|
3294
|
+
format: "uuid",
|
|
3295
|
+
description: "The UUID of the memory to forget.",
|
|
3296
|
+
},
|
|
3297
|
+
},
|
|
3298
|
+
required: ["id"],
|
|
3299
|
+
},
|
|
3300
|
+
},
|
|
3301
|
+
|
|
2990
3302
|
// Knowledge Management Tools (orchestrator key)
|
|
2991
3303
|
{
|
|
2992
3304
|
name: "knowledge_publish",
|
|
@@ -3892,6 +4204,114 @@ const TOOLS = [
|
|
|
3892
4204
|
required: ["name", "slug", "email"],
|
|
3893
4205
|
},
|
|
3894
4206
|
},
|
|
4207
|
+
{
|
|
4208
|
+
name: "request_authenticator_challenge",
|
|
4209
|
+
description:
|
|
4210
|
+
"Step 1 of the opt-in WebAuthn trust-tier upgrade ceremony (US-26.7.2): issues a " +
|
|
4211
|
+
"registration challenge for enrolling a hardware authenticator against an EXISTING " +
|
|
4212
|
+
"agent_rooted (KB-tier) tenant, promoting it to human_anchored on success. " +
|
|
4213
|
+
"IMPORTANT: completing this ceremony requires an INTERACTIVE WebAuthn client " +
|
|
4214
|
+
"(a browser calling navigator.credentials.create(), or a native FIDO2 library) " +
|
|
4215
|
+
"with a HUMAN present to physically touch the authenticator — an agent alone " +
|
|
4216
|
+
"cannot produce a valid attestation, by design. Use this tool to fetch the " +
|
|
4217
|
+
"challenge/rp/user/pubKeyCredParams payload, drive the WebAuthn ceremony in your " +
|
|
4218
|
+
"interactive client, then call enroll_authenticator with the result. If the " +
|
|
4219
|
+
"tenant is already human_anchored, the response includes reauth_required: true " +
|
|
4220
|
+
"plus a reauth_challenge — enroll_authenticator will need a fresh assertion from " +
|
|
4221
|
+
"an EXISTING authenticator too (see enroll_authenticator's description). Requires " +
|
|
4222
|
+
"LOOPCTL_USER_KEY (user role) bound to tenant_id.",
|
|
4223
|
+
inputSchema: {
|
|
4224
|
+
type: "object",
|
|
4225
|
+
properties: {
|
|
4226
|
+
tenant_id: { type: "string", description: "Tenant UUID to enroll an authenticator for." },
|
|
4227
|
+
},
|
|
4228
|
+
required: ["tenant_id"],
|
|
4229
|
+
},
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
name: "enroll_authenticator",
|
|
4233
|
+
description:
|
|
4234
|
+
"Step 2 of the opt-in WebAuthn trust-tier upgrade ceremony: completes enrollment " +
|
|
4235
|
+
"with the attestation produced by navigator.credentials.create() against the " +
|
|
4236
|
+
"challenge from request_authenticator_challenge. On a tenant's FIRST enrollment " +
|
|
4237
|
+
"(zero prior authenticators) this call FLIPS the tenant from agent_rooted to " +
|
|
4238
|
+
"human_anchored, unlocking the work-breakdown / chain-of-custody surface " +
|
|
4239
|
+
"(projects, stories, dispatch, ...). On a SUBSEQUENT (backup) enrollment for an " +
|
|
4240
|
+
"already human_anchored tenant, `reauth_assertion` (a fresh WebAuthn assertion " +
|
|
4241
|
+
"from an EXISTING enrolled authenticator, from the challenge's reauth_challenge) " +
|
|
4242
|
+
"is REQUIRED — omitting it when required returns 401 reauth_required. All " +
|
|
4243
|
+
"binary fields are base64url encoded. Requires LOOPCTL_USER_KEY (user role) " +
|
|
4244
|
+
"bound to tenant_id. Cannot be completed by an agent alone — see " +
|
|
4245
|
+
"request_authenticator_challenge.",
|
|
4246
|
+
inputSchema: {
|
|
4247
|
+
type: "object",
|
|
4248
|
+
properties: {
|
|
4249
|
+
tenant_id: { type: "string", description: "Tenant UUID." },
|
|
4250
|
+
challenge_id: {
|
|
4251
|
+
type: "string",
|
|
4252
|
+
description: "challenge_id from request_authenticator_challenge.",
|
|
4253
|
+
},
|
|
4254
|
+
attestation_object: { type: "string", description: "Base64url attestation object." },
|
|
4255
|
+
client_data_json: { type: "string", description: "Base64url raw client data JSON." },
|
|
4256
|
+
credential_id: { type: "string", description: "Base64url credential id." },
|
|
4257
|
+
friendly_name: {
|
|
4258
|
+
type: "string",
|
|
4259
|
+
description: "Operator-supplied label (1..120 chars, default \"Authenticator\").",
|
|
4260
|
+
},
|
|
4261
|
+
reauth_assertion: {
|
|
4262
|
+
type: "object",
|
|
4263
|
+
description:
|
|
4264
|
+
"Required ONLY when enrolling a backup authenticator on an already " +
|
|
4265
|
+
"human_anchored tenant: a fresh assertion (challenge_id, credential_id, " +
|
|
4266
|
+
"authenticator_data, signature, client_data_json — all base64url) from an " +
|
|
4267
|
+
"EXISTING enrolled authenticator, bound to the reauth_challenge returned by " +
|
|
4268
|
+
"request_authenticator_challenge.",
|
|
4269
|
+
},
|
|
4270
|
+
},
|
|
4271
|
+
required: ["tenant_id", "challenge_id", "attestation_object", "client_data_json", "credential_id"],
|
|
4272
|
+
},
|
|
4273
|
+
},
|
|
4274
|
+
{
|
|
4275
|
+
name: "request_authenticator_revoke_challenge",
|
|
4276
|
+
description:
|
|
4277
|
+
"Issues a fresh-assertion challenge to authorize revoking one of a tenant's " +
|
|
4278
|
+
"enrolled WebAuthn authenticators. Requires an INTERACTIVE WebAuthn client + " +
|
|
4279
|
+
"human touch to produce the assertion revoke_authenticator needs — an agent " +
|
|
4280
|
+
"alone cannot authorize a revocation. Requires LOOPCTL_USER_KEY (user role) " +
|
|
4281
|
+
"bound to tenant_id.",
|
|
4282
|
+
inputSchema: {
|
|
4283
|
+
type: "object",
|
|
4284
|
+
properties: {
|
|
4285
|
+
tenant_id: { type: "string", description: "Tenant UUID." },
|
|
4286
|
+
},
|
|
4287
|
+
required: ["tenant_id"],
|
|
4288
|
+
},
|
|
4289
|
+
},
|
|
4290
|
+
{
|
|
4291
|
+
name: "revoke_authenticator",
|
|
4292
|
+
description:
|
|
4293
|
+
"Revokes an enrolled authenticator using the assertion from " +
|
|
4294
|
+
"request_authenticator_revoke_challenge (navigator.credentials.get() against an " +
|
|
4295
|
+
"EXISTING authenticator — human touch required). Refuses (409 last_authenticator) " +
|
|
4296
|
+
"to remove a human_anchored tenant's LAST authenticator — there is no " +
|
|
4297
|
+
"auto-downgrade; losing every authenticator is a fatal, human-recovery-only event " +
|
|
4298
|
+
"(docs/chain-of-custody-v2.md §9.3). Requires LOOPCTL_USER_KEY (user role) bound " +
|
|
4299
|
+
"to tenant_id.",
|
|
4300
|
+
inputSchema: {
|
|
4301
|
+
type: "object",
|
|
4302
|
+
properties: {
|
|
4303
|
+
tenant_id: { type: "string", description: "Tenant UUID." },
|
|
4304
|
+
authenticator_id: { type: "string", description: "UUID of the authenticator to revoke." },
|
|
4305
|
+
webauthn_assertion: {
|
|
4306
|
+
type: "object",
|
|
4307
|
+
description:
|
|
4308
|
+
"The assertion (challenge_id, credential_id, authenticator_data, signature, " +
|
|
4309
|
+
"client_data_json — all base64url) bound to the revoke-challenge.",
|
|
4310
|
+
},
|
|
4311
|
+
},
|
|
4312
|
+
required: ["tenant_id", "authenticator_id", "webauthn_assertion"],
|
|
4313
|
+
},
|
|
4314
|
+
},
|
|
3895
4315
|
{
|
|
3896
4316
|
name: "get_sth",
|
|
3897
4317
|
description: "Get the latest Signed Tree Head for a tenant's audit chain. Public — no auth required.",
|
|
@@ -4090,6 +4510,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4090
4510
|
case "knowledge_create":
|
|
4091
4511
|
return await knowledgeCreate(args);
|
|
4092
4512
|
|
|
4513
|
+
// Agent Memory Tools
|
|
4514
|
+
case "memory_remember":
|
|
4515
|
+
return await memoryRemember(args);
|
|
4516
|
+
|
|
4517
|
+
case "memory_recall":
|
|
4518
|
+
return await memoryRecall(args);
|
|
4519
|
+
|
|
4520
|
+
case "memory_list":
|
|
4521
|
+
return await memoryList(args);
|
|
4522
|
+
|
|
4523
|
+
case "memory_forget":
|
|
4524
|
+
return await memoryForget(args);
|
|
4525
|
+
|
|
4093
4526
|
// Knowledge Management Tools
|
|
4094
4527
|
case "knowledge_publish":
|
|
4095
4528
|
return await knowledgePublish(args);
|
|
@@ -4182,6 +4615,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4182
4615
|
case "signup":
|
|
4183
4616
|
return await signup(args);
|
|
4184
4617
|
|
|
4618
|
+
case "request_authenticator_challenge":
|
|
4619
|
+
return await requestAuthenticatorChallenge(args);
|
|
4620
|
+
|
|
4621
|
+
case "enroll_authenticator":
|
|
4622
|
+
return await enrollAuthenticator(args);
|
|
4623
|
+
|
|
4624
|
+
case "request_authenticator_revoke_challenge":
|
|
4625
|
+
return await requestAuthenticatorRevokeChallenge(args);
|
|
4626
|
+
|
|
4627
|
+
case "revoke_authenticator":
|
|
4628
|
+
return await revokeAuthenticator(args);
|
|
4629
|
+
|
|
4185
4630
|
case "get_sth":
|
|
4186
4631
|
return await getSth(args);
|
|
4187
4632
|
|
package/lib/http-helpers.js
CHANGED
|
@@ -68,6 +68,24 @@ export function llmUsagePath({ from, to, limit, offset } = {}) {
|
|
|
68
68
|
])}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Path for `memory_list`, honoring limit/offset/include_superseded/all_subjects
|
|
73
|
+
* (US-28.4). Routes through the shared `buildQuery` helper (rather than a local
|
|
74
|
+
* `URLSearchParams` mirror) so the query-string construction is exercised by the
|
|
75
|
+
* SAME code the server ships.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ limit?: number, offset?: number, include_superseded?: boolean, all_subjects?: boolean }} [args]
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function memoryPath({ limit, offset, include_superseded, all_subjects } = {}) {
|
|
81
|
+
return `/api/v1/memory${buildQuery([
|
|
82
|
+
["limit", limit],
|
|
83
|
+
["offset", offset],
|
|
84
|
+
["include_superseded", include_superseded],
|
|
85
|
+
["all_subjects", all_subjects],
|
|
86
|
+
])}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
/**
|
|
72
90
|
* Defensively parse the raw text body of a JSON-content-type HTTP response
|
|
73
91
|
* (#249, mcp-03).
|