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/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
- // D2 v1.12.10: optional ?tenant=<t> param. Defaults to ctx.tenantId
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 effectiveCtx = tenantOverride !== null && tenantOverride !== ''
1202
- ? { ...ctx, tenantId: tenantOverride }
1203
- : ctx;
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.48.0";
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.48.0';
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) => {
@@ -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.48.0",
5
+ "version": "1.50.0",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "type": "module",
5
5
  "description": "Hippo Memory plugin for OpenClaw - biologically-inspired agent memory",
6
6
  "main": "index.ts",
@@ -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.48.0",
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.48.0",
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",