hippo-memory 1.48.0 → 1.50.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/dist/api.d.ts +36 -0
- package/dist/api.js +185 -9
- package/dist/audit.d.ts +1 -1
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +20 -1
- package/dist/cli.js +118 -2
- package/dist/connectors/github/ingest.js +1 -0
- package/dist/connectors/slack/ingest.js +1 -0
- package/dist/consolidate.d.ts +2 -0
- package/dist/consolidate.js +20 -3
- package/dist/dag.js +15 -6
- package/dist/db.js +42 -1
- package/dist/dedupe.js +5 -2
- package/dist/extract.js +1 -0
- package/dist/instruction-detect.d.ts +7 -0
- package/dist/instruction-detect.js +27 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +6 -17
- package/dist/project-briefs.js +4 -2
- package/dist/quarantine.d.ts +31 -0
- package/dist/quarantine.js +63 -0
- package/dist/recall-scope.d.ts +22 -10
- package/dist/recall-scope.js +41 -26
- package/dist/secret-detect.js +1 -1
- package/dist/server.js +69 -10
- package/dist/shared.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -22,7 +22,7 @@ export function __resetSessionRecallHistoryHttp() {
|
|
|
22
22
|
import { PACKAGE_VERSION } from './version.js';
|
|
23
23
|
import { validateApiKey } from './auth.js';
|
|
24
24
|
import { createRateLimiter } from './rate-limit.js';
|
|
25
|
-
import { remember, retrieve, RecallContractError, ForbiddenError, drillDown, assemble, forget, promote, supersede, archiveRaw, authCreate, authList, authRevoke, auditList, outcome, outcomeForLastRecall, getContext, sleep, adminActor, recordTokens, } from './api.js';
|
|
25
|
+
import { remember, retrieve, RecallContractError, ForbiddenError, drillDown, assemble, forget, promote, supersede, archiveRaw, authCreate, authList, authRevoke, auditList, outcome, outcomeForLastRecall, getContext, sleep, adminActor, recordTokens, quarantineList, quarantineApprove, quarantineReject, } from './api.js';
|
|
26
26
|
import { buildGraphModel } from './graph-view.js';
|
|
27
27
|
import { MAX_ENTITY_NAME_LEN } from './graph.js';
|
|
28
28
|
import { savePrediction, closePrediction, loadPredictionById, loadPredictionsByClass, loadOpenPredictions, computePredictionBaserate, VALID_CLOSURE_STATES, } from './predictions.js';
|
|
@@ -116,6 +116,11 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
116
116
|
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
117
117
|
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
118
118
|
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
119
|
+
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
120
|
+
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
121
|
+
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
122
|
+
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
123
|
+
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
119
124
|
]);
|
|
120
125
|
// Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
|
|
121
126
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
@@ -469,6 +474,7 @@ function buildContextWithAuth(req, hippoRoot) {
|
|
|
469
474
|
actor: {
|
|
470
475
|
subject: `api_key:${result.keyId}`,
|
|
471
476
|
role: result.role,
|
|
477
|
+
scopes: result.scopes,
|
|
472
478
|
},
|
|
473
479
|
};
|
|
474
480
|
}
|
|
@@ -1160,6 +1166,61 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1160
1166
|
sendJson(res, 200, result);
|
|
1161
1167
|
return;
|
|
1162
1168
|
}
|
|
1169
|
+
// GET /v1/quarantine?status=: CD5 review queue. quarantineList carries no role gate itself, so it's checked here.
|
|
1170
|
+
if (method === 'GET' && path === '/v1/quarantine') {
|
|
1171
|
+
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1172
|
+
if (ctx.actor.role !== 'admin') {
|
|
1173
|
+
throw new HttpError(403, '/v1/quarantine requires admin role');
|
|
1174
|
+
}
|
|
1175
|
+
const statusRaw = query.get('status');
|
|
1176
|
+
let status = 'pending';
|
|
1177
|
+
if (statusRaw !== null) {
|
|
1178
|
+
if (statusRaw !== 'pending' && statusRaw !== 'approved' && statusRaw !== 'rejected' && statusRaw !== 'all') {
|
|
1179
|
+
throw new HttpError(400, 'status must be one of: pending | approved | rejected | all');
|
|
1180
|
+
}
|
|
1181
|
+
status = statusRaw;
|
|
1182
|
+
}
|
|
1183
|
+
sendJson(res, 200, { quarantine: quarantineList(ctx, { status }) });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
// POST /v1/quarantine/:id/approve: admin only; ForbiddenError falls through to mapApiError's 403.
|
|
1187
|
+
const quarantineApproveMatch = matchPath('/v1/quarantine/:id/approve', path);
|
|
1188
|
+
if (method === 'POST' && quarantineApproveMatch) {
|
|
1189
|
+
validateIdSegment(quarantineApproveMatch.id, 'memory id');
|
|
1190
|
+
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1191
|
+
try {
|
|
1192
|
+
quarantineApprove(ctx, quarantineApproveMatch.id);
|
|
1193
|
+
sendJson(res, 200, { approved: quarantineApproveMatch.id });
|
|
1194
|
+
}
|
|
1195
|
+
catch (e) {
|
|
1196
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1197
|
+
if (msg.includes('not quarantined'))
|
|
1198
|
+
throw new HttpError(404, msg);
|
|
1199
|
+
if (msg.includes('is already') || msg.includes('scope changed'))
|
|
1200
|
+
throw new HttpError(409, msg);
|
|
1201
|
+
throw e;
|
|
1202
|
+
}
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
// POST /v1/quarantine/:id/reject: admin only; ForbiddenError falls through to mapApiError's 403.
|
|
1206
|
+
const quarantineRejectMatch = matchPath('/v1/quarantine/:id/reject', path);
|
|
1207
|
+
if (method === 'POST' && quarantineRejectMatch) {
|
|
1208
|
+
validateIdSegment(quarantineRejectMatch.id, 'memory id');
|
|
1209
|
+
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1210
|
+
try {
|
|
1211
|
+
quarantineReject(ctx, quarantineRejectMatch.id);
|
|
1212
|
+
sendJson(res, 200, { rejected: quarantineRejectMatch.id });
|
|
1213
|
+
}
|
|
1214
|
+
catch (e) {
|
|
1215
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1216
|
+
if (msg.includes('not quarantined'))
|
|
1217
|
+
throw new HttpError(404, msg);
|
|
1218
|
+
if (msg.includes('is already'))
|
|
1219
|
+
throw new HttpError(409, msg);
|
|
1220
|
+
throw e;
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1163
1224
|
// GET /v1/audit?op=&since=&limit= — read audit events. All three filters
|
|
1164
1225
|
// validated at the route boundary so an invalid value lands a 400 before
|
|
1165
1226
|
// we hit the DB.
|
|
@@ -1191,16 +1252,13 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1191
1252
|
limit = parsed;
|
|
1192
1253
|
}
|
|
1193
1254
|
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1194
|
-
//
|
|
1195
|
-
// (caller's own tenant). Lets admins query the '__host__' synthetic
|
|
1196
|
-
// tenant where host-wide ops like 'consolidate' are recorded. Today
|
|
1197
|
-
// the audit route inherits the auth gate on /v1/audit (Bearer auth);
|
|
1198
|
-
// a future per-tenant authz check should ensure non-admin Bearers
|
|
1199
|
-
// can only pass their own tenant_id here.
|
|
1255
|
+
// ?tenant=<t> reads another tenant (e.g. '__host__' for consolidate rows); admin only.
|
|
1200
1256
|
const tenantOverride = query.get('tenant');
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1257
|
+
const crossTenant = tenantOverride !== null && tenantOverride !== '' && tenantOverride !== ctx.tenantId;
|
|
1258
|
+
if (crossTenant && ctx.actor.role !== 'admin') {
|
|
1259
|
+
throw new HttpError(403, '/v1/audit?tenant= for another tenant requires admin role');
|
|
1260
|
+
}
|
|
1261
|
+
const effectiveCtx = crossTenant ? { ...ctx, tenantId: tenantOverride } : ctx;
|
|
1204
1262
|
const result = auditList(effectiveCtx, { op, since, limit });
|
|
1205
1263
|
sendJson(res, 200, result);
|
|
1206
1264
|
return;
|
|
@@ -3021,6 +3079,7 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
3021
3079
|
actor: ctx.actor.subject,
|
|
3022
3080
|
// The caller's real role: MCP tools must not run a member key as admin.
|
|
3023
3081
|
role: ctx.actor.role,
|
|
3082
|
+
scopes: ctx.actor.scopes,
|
|
3024
3083
|
clientKey: buildMcpClientKey(req),
|
|
3025
3084
|
});
|
|
3026
3085
|
}
|
package/dist/shared.js
CHANGED
|
@@ -13,6 +13,7 @@ import { search, hybridSearch } from './search.js';
|
|
|
13
13
|
import { evalNow } from './ablation.js';
|
|
14
14
|
import { deriveOriginProject, classifyOriginProject, resolveGlobalRootDir } from './project-identity.js';
|
|
15
15
|
import { detectSecret } from './secret-detect.js';
|
|
16
|
+
import { isQuarantineScope } from './quarantine.js';
|
|
16
17
|
import { RejectedValueError } from './rejection.js';
|
|
17
18
|
import { embedMemory, embedAll } from './embeddings.js';
|
|
18
19
|
/**
|
|
@@ -46,6 +47,10 @@ export function promoteToGlobal(localRoot, id, opts) {
|
|
|
46
47
|
const entry = readEntry(localRoot, id, opts?.tenantId);
|
|
47
48
|
if (!entry)
|
|
48
49
|
throw new Error(`Memory not found: ${id}`);
|
|
50
|
+
// CD5: same veto as shareMemory; a promoted copy would have no quarantine record to review.
|
|
51
|
+
if (isQuarantineScope(entry.scope)) {
|
|
52
|
+
throw new Error(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
53
|
+
}
|
|
49
54
|
// v39 S4 producer veto: promote is a producer path to the global store
|
|
50
55
|
// exactly like shareMemory - same hard rule (codex gating review P2).
|
|
51
56
|
const promoteSecret = detectSecret(entry);
|
|
@@ -272,6 +277,10 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
272
277
|
throw new Error(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
|
|
273
278
|
`Secrets stay in their owning project's store.`);
|
|
274
279
|
}
|
|
280
|
+
// CD5: a quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
|
|
281
|
+
if (isQuarantineScope(entry.scope)) {
|
|
282
|
+
throw new Error(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
283
|
+
}
|
|
275
284
|
const score = transferScore(entry);
|
|
276
285
|
if (score < 0.3 && !options.force)
|
|
277
286
|
return null;
|
|
@@ -385,6 +394,9 @@ export function autoShare(localRoot, options = {}) {
|
|
|
385
394
|
// Build set of global content hashes to avoid duplicates
|
|
386
395
|
const globalContentSet = new Set(globalEntries.map((e) => e.content.toLowerCase().trim().slice(0, 200)));
|
|
387
396
|
const candidates = localEntries.filter((entry) => {
|
|
397
|
+
// CD5: shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
|
|
398
|
+
if (isQuarantineScope(entry.scope ?? null))
|
|
399
|
+
return false;
|
|
388
400
|
const score = transferScore(entry);
|
|
389
401
|
if (score < minScore)
|
|
390
402
|
return false;
|
package/dist/version.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export declare const PACKAGE_VERSION = "1.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.50.0";
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export declare function compareSemver(a: string, b: string): number;
|
|
22
22
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export const PACKAGE_VERSION = '1.
|
|
19
|
+
export const PACKAGE_VERSION = '1.50.0';
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export function compareSemver(a, b) {
|
|
22
22
|
const parse = (v) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hippo-memory",
|
|
3
3
|
"name": "Hippo Memory",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Decay by default, retrieval strengthening, sleep consolidation.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.50.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hippo-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Zero runtime deps, SQLite, MCP server, and an opt-in hosted TypeSafe Jev reranker. Decay, retrieval strengthening, consolidation.",
|
|
5
5
|
"mcpName": "io.github.kitfunso/hippo-memory",
|
|
6
6
|
"type": "module",
|