hippo-memory 1.49.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 +26 -0
- package/dist/api.js +134 -3
- package/dist/audit.d.ts +1 -1
- package/dist/cli.js +62 -0
- package/dist/connectors/github/ingest.js +1 -0
- package/dist/connectors/slack/ingest.js +1 -0
- package/dist/consolidate.js +3 -0
- package/dist/db.js +26 -1
- package/dist/instruction-detect.d.ts +7 -0
- package/dist/instruction-detect.js +27 -0
- package/dist/quarantine.d.ts +31 -0
- package/dist/quarantine.js +63 -0
- package/dist/server.js +59 -1
- 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/api.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { deleteEntry, loadAllEntries, type TaskSnapshot, type SessionEvent } fro
|
|
|
11
11
|
import { type RejectedValueRow } from './rejection.js';
|
|
12
12
|
import { type DormantMemory, type ListDormantOpts } from './dormant.js';
|
|
13
13
|
import { type TokenSummary, type TokenSurface } from './token-ledger.js';
|
|
14
|
+
import { type QuarantineStatus } from './quarantine.js';
|
|
14
15
|
import { type FailureSummary } from './failure-log.js';
|
|
15
16
|
import { type SessionHandoff } from './handoff.js';
|
|
16
17
|
import { type MemoryKind, type MemoryEntry } from './memory.js';
|
|
@@ -110,11 +111,17 @@ export interface RememberOpts {
|
|
|
110
111
|
* back and the error is rethrown.
|
|
111
112
|
*/
|
|
112
113
|
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
114
|
+
/** CD5: connector-ingested content an agent doesn't control; gates detectInstruction. CLI/HTTP/MCP never set this. */
|
|
115
|
+
untrusted?: boolean;
|
|
113
116
|
}
|
|
114
117
|
export interface RememberResult {
|
|
115
118
|
id: string;
|
|
116
119
|
kind: MemoryKind;
|
|
117
120
|
tenantId: string;
|
|
121
|
+
/** Set only when untrusted content was flagged and quarantined instead of stored under its requested scope. */
|
|
122
|
+
quarantined?: {
|
|
123
|
+
reason: string;
|
|
124
|
+
};
|
|
118
125
|
}
|
|
119
126
|
export declare function remember(ctx: Context, opts: RememberOpts): RememberResult;
|
|
120
127
|
export interface RecallOpts {
|
|
@@ -1061,6 +1068,25 @@ export declare function restoreDormant(ctx: Context, id: string): MemoryEntry;
|
|
|
1061
1068
|
export declare function forgetDormant(ctx: Context, id: string): void;
|
|
1062
1069
|
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
1063
1070
|
export declare function isDormant(ctx: Context, id: string): boolean;
|
|
1071
|
+
export interface QuarantineListItem {
|
|
1072
|
+
id: string;
|
|
1073
|
+
originalScope: string | null;
|
|
1074
|
+
reason: string;
|
|
1075
|
+
status: QuarantineStatus;
|
|
1076
|
+
quarantinedAt: string;
|
|
1077
|
+
decidedAt: string | null;
|
|
1078
|
+
decidedBy: string | null;
|
|
1079
|
+
contentPreview: string;
|
|
1080
|
+
}
|
|
1081
|
+
/** A tenant's quarantined memories, newest first. Default `status` is 'pending' (the review queue). */
|
|
1082
|
+
export declare function quarantineList(ctx: Context, opts?: {
|
|
1083
|
+
status?: QuarantineStatus | 'all';
|
|
1084
|
+
limit?: number;
|
|
1085
|
+
}): QuarantineListItem[];
|
|
1086
|
+
/** Release a quarantined memory to its original scope. Admin only; the scope guard refuses a row moved since (mirrors restoreDormant). */
|
|
1087
|
+
export declare function quarantineApprove(ctx: Context, id: string): void;
|
|
1088
|
+
/** Keep a quarantined memory hidden for good. Admin only; the raw row is untouched (append-only). */
|
|
1089
|
+
export declare function quarantineReject(ctx: Context, id: string): void;
|
|
1064
1090
|
export interface SleepResult {
|
|
1065
1091
|
active: number;
|
|
1066
1092
|
removed: number;
|
package/dist/api.js
CHANGED
|
@@ -13,6 +13,8 @@ import { RejectedValueError } from './rejection.js';
|
|
|
13
13
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
14
14
|
import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow, } from './dormant.js';
|
|
15
15
|
import { recordTokenUse, summarizeTokenUse } from './token-ledger.js';
|
|
16
|
+
import { detectInstruction } from './instruction-detect.js';
|
|
17
|
+
import { quarantineScopeFor, recordQuarantine, getQuarantineRow, listQuarantineRows, approveQuarantineRow, rejectQuarantineRow, } from './quarantine.js';
|
|
16
18
|
import { summarizeFailures } from './failure-log.js';
|
|
17
19
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
18
20
|
import { createMemory, applyOutcome, calculateStrength, Layer, } from './memory.js';
|
|
@@ -140,9 +142,11 @@ function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admi
|
|
|
140
142
|
return loadAmbientCandidates(hippoRoot, tenantId, includeRecent, admitAmbient);
|
|
141
143
|
}
|
|
142
144
|
export function remember(ctx, opts) {
|
|
145
|
+
const detection = opts.untrusted ? detectInstruction(opts.content) : { flagged: false, reason: null };
|
|
146
|
+
const requestedScope = opts.scope ?? null;
|
|
143
147
|
const entry = createMemory(opts.content, {
|
|
144
148
|
kind: opts.kind ?? 'distilled',
|
|
145
|
-
scope:
|
|
149
|
+
scope: detection.flagged ? quarantineScopeFor(requestedScope) : requestedScope,
|
|
146
150
|
owner: opts.owner ?? null,
|
|
147
151
|
artifact_ref: opts.artifactRef ?? null,
|
|
148
152
|
tags: opts.tags,
|
|
@@ -151,8 +155,23 @@ export function remember(ctx, opts) {
|
|
|
151
155
|
});
|
|
152
156
|
// writeEntry threads ctx.actor.subject into its internal audit hook, so exactly
|
|
153
157
|
// one 'remember' event lands in the log with the supplied actor.
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
const afterWrite = detection.flagged
|
|
159
|
+
? (db, memoryId) => {
|
|
160
|
+
recordQuarantine(db, {
|
|
161
|
+
tenantId: ctx.tenantId,
|
|
162
|
+
memoryId,
|
|
163
|
+
originalScope: requestedScope,
|
|
164
|
+
reason: detection.reason ?? 'unknown',
|
|
165
|
+
actor: ctx.actor.subject,
|
|
166
|
+
});
|
|
167
|
+
opts.afterWrite?.(db, memoryId);
|
|
168
|
+
}
|
|
169
|
+
: opts.afterWrite;
|
|
170
|
+
writeEntry(ctx.hippoRoot, entry, { actor: ctx.actor.subject, afterWrite });
|
|
171
|
+
const result = { id: entry.id, kind: entry.kind, tenantId: ctx.tenantId };
|
|
172
|
+
if (detection.flagged)
|
|
173
|
+
result.quarantined = { reason: detection.reason ?? 'unknown' };
|
|
174
|
+
return result;
|
|
156
175
|
}
|
|
157
176
|
/**
|
|
158
177
|
* Shared construction helper for `RecallSuppressionSummary`. Used by
|
|
@@ -2090,6 +2109,118 @@ export function isDormant(ctx, id) {
|
|
|
2090
2109
|
closeHippoDb(db);
|
|
2091
2110
|
}
|
|
2092
2111
|
}
|
|
2112
|
+
const QUARANTINE_PREVIEW_CHARS = 200;
|
|
2113
|
+
/** A tenant's quarantined memories, newest first. Default `status` is 'pending' (the review queue). */
|
|
2114
|
+
export function quarantineList(ctx, opts = {}) {
|
|
2115
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2116
|
+
try {
|
|
2117
|
+
const rows = listQuarantineRows(db, ctx.tenantId, opts.status ?? 'pending', opts.limit);
|
|
2118
|
+
return rows.map((row) => {
|
|
2119
|
+
const entry = readEntry(ctx.hippoRoot, row.memoryId, ctx.tenantId);
|
|
2120
|
+
return {
|
|
2121
|
+
id: row.memoryId,
|
|
2122
|
+
originalScope: row.originalScope,
|
|
2123
|
+
reason: row.reason,
|
|
2124
|
+
status: row.status,
|
|
2125
|
+
quarantinedAt: row.quarantinedAt,
|
|
2126
|
+
decidedAt: row.decidedAt,
|
|
2127
|
+
decidedBy: row.decidedBy,
|
|
2128
|
+
contentPreview: entry ? entry.content.slice(0, QUARANTINE_PREVIEW_CHARS) : '',
|
|
2129
|
+
};
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
finally {
|
|
2133
|
+
closeHippoDb(db);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
function loadPendingQuarantineRow(db, tenantId, id) {
|
|
2137
|
+
const row = getQuarantineRow(db, tenantId, id);
|
|
2138
|
+
if (!row)
|
|
2139
|
+
throw new Error(`not quarantined: ${id}`);
|
|
2140
|
+
if (row.status !== 'pending')
|
|
2141
|
+
throw new Error(`${id} is already ${row.status}`);
|
|
2142
|
+
return row;
|
|
2143
|
+
}
|
|
2144
|
+
/** Release a quarantined memory to its original scope. Admin only; the scope guard refuses a row moved since (mirrors restoreDormant). */
|
|
2145
|
+
export function quarantineApprove(ctx, id) {
|
|
2146
|
+
if (ctx.actor.role !== 'admin') {
|
|
2147
|
+
throw new ForbiddenError('Only an admin key can approve a quarantined memory');
|
|
2148
|
+
}
|
|
2149
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2150
|
+
try {
|
|
2151
|
+
db.exec('BEGIN IMMEDIATE');
|
|
2152
|
+
try {
|
|
2153
|
+
const row = loadPendingQuarantineRow(db, ctx.tenantId, id);
|
|
2154
|
+
const quarantineScope = quarantineScopeFor(row.originalScope);
|
|
2155
|
+
const updated = db
|
|
2156
|
+
.prepare(`UPDATE memories SET scope = ? WHERE id = ? AND tenant_id = ? AND scope = ?`)
|
|
2157
|
+
.run(row.originalScope, id, ctx.tenantId, quarantineScope);
|
|
2158
|
+
if (Number(updated.changes ?? 0) !== 1) {
|
|
2159
|
+
throw new Error(`memory ${id} scope changed since quarantine; refusing to approve`);
|
|
2160
|
+
}
|
|
2161
|
+
approveQuarantineRow(db, ctx.tenantId, id, ctx.actor.subject);
|
|
2162
|
+
appendAuditEvent(db, {
|
|
2163
|
+
tenantId: ctx.tenantId,
|
|
2164
|
+
actor: ctx.actor.subject,
|
|
2165
|
+
op: 'quarantine_approve',
|
|
2166
|
+
targetId: id,
|
|
2167
|
+
metadata: { originalScope: row.originalScope },
|
|
2168
|
+
});
|
|
2169
|
+
db.exec('COMMIT');
|
|
2170
|
+
}
|
|
2171
|
+
catch (err) {
|
|
2172
|
+
try {
|
|
2173
|
+
db.exec('ROLLBACK');
|
|
2174
|
+
}
|
|
2175
|
+
catch { /* already rolled back */ }
|
|
2176
|
+
throw err;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
finally {
|
|
2180
|
+
closeHippoDb(db);
|
|
2181
|
+
}
|
|
2182
|
+
// Post-commit, best-effort: a failed rewrite leaves the mirror showing the quarantine scope (fail-closed).
|
|
2183
|
+
try {
|
|
2184
|
+
const restored = readEntry(ctx.hippoRoot, id, ctx.tenantId);
|
|
2185
|
+
if (restored)
|
|
2186
|
+
writeEntryMirrors(ctx.hippoRoot, restored);
|
|
2187
|
+
}
|
|
2188
|
+
catch (err) {
|
|
2189
|
+
console.error(`quarantine: mirror rewrite failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/** Keep a quarantined memory hidden for good. Admin only; the raw row is untouched (append-only). */
|
|
2193
|
+
export function quarantineReject(ctx, id) {
|
|
2194
|
+
if (ctx.actor.role !== 'admin') {
|
|
2195
|
+
throw new ForbiddenError('Only an admin key can reject a quarantined memory');
|
|
2196
|
+
}
|
|
2197
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2198
|
+
try {
|
|
2199
|
+
db.exec('BEGIN IMMEDIATE');
|
|
2200
|
+
try {
|
|
2201
|
+
loadPendingQuarantineRow(db, ctx.tenantId, id);
|
|
2202
|
+
rejectQuarantineRow(db, ctx.tenantId, id, ctx.actor.subject);
|
|
2203
|
+
appendAuditEvent(db, {
|
|
2204
|
+
tenantId: ctx.tenantId,
|
|
2205
|
+
actor: ctx.actor.subject,
|
|
2206
|
+
op: 'quarantine_reject',
|
|
2207
|
+
targetId: id,
|
|
2208
|
+
metadata: {},
|
|
2209
|
+
});
|
|
2210
|
+
db.exec('COMMIT');
|
|
2211
|
+
}
|
|
2212
|
+
catch (err) {
|
|
2213
|
+
try {
|
|
2214
|
+
db.exec('ROLLBACK');
|
|
2215
|
+
}
|
|
2216
|
+
catch { /* already rolled back */ }
|
|
2217
|
+
throw err;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
finally {
|
|
2221
|
+
closeHippoDb(db);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2093
2224
|
const DEFAULT_SLEEP_PHASES = {
|
|
2094
2225
|
consolidate,
|
|
2095
2226
|
deduplicateStore,
|
package/dist/audit.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface AuditResult {
|
|
|
16
16
|
export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
|
|
17
17
|
export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
|
|
18
18
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
19
|
-
export type AuditOp = 'remember' | 'recall' | 'promote' | 'supersede' | 'forget' | 'archive_raw' | 'auth_revoke' | 'auth_create' | 'outcome' | 'consolidate' | 'audit_prune' | 'summary_marked_dirty' | 'summary_marked_clean' | 'summary_rebuilt' | 'predict_create' | 'predict_close' | 'predict_baserate' | 'recall_autodebias_hint' | 'recall_autodebias_hint_no_class_match' | 'recall_autodebias_hint_tiebreak' | 'recall_anchor_detected_query_repeat' | 'recall_anchor_detected_memory_dominance' | 'recall_anchor_skipped_no_session' | 'recall_availability_detected' | 'decision_create' | 'decision_supersede' | 'decision_close' | 'incident_open' | 'incident_resolve' | 'incident_close' | 'process_create' | 'process_supersede' | 'process_close' | 'policy_create' | 'policy_supersede' | 'policy_close' | 'skill_create' | 'skill_supersede' | 'skill_close' | 'project_brief_create' | 'project_brief_supersede' | 'project_brief_close' | 'customer_note_create' | 'customer_note_supersede' | 'customer_note_close' | 'mv_rescue' | 'reject_value' | 'reject_refusal' | 'unreject_value' | 'half_life_migrate' | 'dormant_restore' | 'conflict_resolve' | 'auth_grant' | 'auth_ungrant';
|
|
19
|
+
export type AuditOp = 'remember' | 'recall' | 'promote' | 'supersede' | 'forget' | 'archive_raw' | 'auth_revoke' | 'auth_create' | 'outcome' | 'consolidate' | 'audit_prune' | 'summary_marked_dirty' | 'summary_marked_clean' | 'summary_rebuilt' | 'predict_create' | 'predict_close' | 'predict_baserate' | 'recall_autodebias_hint' | 'recall_autodebias_hint_no_class_match' | 'recall_autodebias_hint_tiebreak' | 'recall_anchor_detected_query_repeat' | 'recall_anchor_detected_memory_dominance' | 'recall_anchor_skipped_no_session' | 'recall_availability_detected' | 'decision_create' | 'decision_supersede' | 'decision_close' | 'incident_open' | 'incident_resolve' | 'incident_close' | 'process_create' | 'process_supersede' | 'process_close' | 'policy_create' | 'policy_supersede' | 'policy_close' | 'skill_create' | 'skill_supersede' | 'skill_close' | 'project_brief_create' | 'project_brief_supersede' | 'project_brief_close' | 'customer_note_create' | 'customer_note_supersede' | 'customer_note_close' | 'mv_rescue' | 'reject_value' | 'reject_refusal' | 'unreject_value' | 'half_life_migrate' | 'dormant_restore' | 'conflict_resolve' | 'auth_grant' | 'auth_ungrant' | 'quarantine' | 'quarantine_approve' | 'quarantine_reject';
|
|
20
20
|
export interface AppendAuditOpts {
|
|
21
21
|
tenantId: string;
|
|
22
22
|
actor: string;
|
package/dist/cli.js
CHANGED
|
@@ -3897,6 +3897,56 @@ function cmdDormant(hippoRoot, args, flags) {
|
|
|
3897
3897
|
}
|
|
3898
3898
|
console.log('Bring one back: hippo dormant restore <id> Delete for good: hippo dormant forget <id>');
|
|
3899
3899
|
}
|
|
3900
|
+
/** `hippo quarantine [list] [--all] [--json] [--global]`, `quarantine approve <id>`, `quarantine reject <id>` (CD5 poisoning defence). */
|
|
3901
|
+
function cmdQuarantine(hippoRoot, args, flags) {
|
|
3902
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3903
|
+
const ctx = {
|
|
3904
|
+
hippoRoot: root,
|
|
3905
|
+
tenantId: resolveTenantId({}),
|
|
3906
|
+
actor: api.adminActor('cli'),
|
|
3907
|
+
};
|
|
3908
|
+
const sub = args[0];
|
|
3909
|
+
if (sub === 'approve' || sub === 'reject') {
|
|
3910
|
+
const id = (args[1] ?? '').trim();
|
|
3911
|
+
if (!id) {
|
|
3912
|
+
console.error(`Usage: hippo quarantine ${sub} <id>`);
|
|
3913
|
+
process.exit(1);
|
|
3914
|
+
}
|
|
3915
|
+
try {
|
|
3916
|
+
if (sub === 'approve') {
|
|
3917
|
+
api.quarantineApprove(ctx, id);
|
|
3918
|
+
console.log(`Approved ${id}: restored to its original scope.`);
|
|
3919
|
+
}
|
|
3920
|
+
else {
|
|
3921
|
+
api.quarantineReject(ctx, id);
|
|
3922
|
+
console.log(`Rejected ${id}: stays quarantined.`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
catch (err) {
|
|
3926
|
+
console.error(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3927
|
+
process.exit(1);
|
|
3928
|
+
}
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
const status = flags['all'] ? 'all' : 'pending';
|
|
3932
|
+
const rows = api.quarantineList(ctx, { status });
|
|
3933
|
+
if (flags['json']) {
|
|
3934
|
+
console.log(JSON.stringify({ quarantine: rows }, null, 2));
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
if (rows.length === 0) {
|
|
3938
|
+
console.log(status === 'all' ? 'No quarantined memories.' : 'No pending quarantined memories.');
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3941
|
+
console.log(`${rows.length} quarantined memor${rows.length === 1 ? 'y' : 'ies'} (newest first):\n`);
|
|
3942
|
+
for (const row of rows) {
|
|
3943
|
+
console.log(`--- ${row.id} [${row.status}]`);
|
|
3944
|
+
console.log(` ${row.contentPreview}`);
|
|
3945
|
+
console.log(` ${row.reason}, original scope ${row.originalScope ?? '(none)'}, quarantined ${row.quarantinedAt.slice(0, 10)}`);
|
|
3946
|
+
console.log('');
|
|
3947
|
+
}
|
|
3948
|
+
console.log('Approve: hippo quarantine approve <id> Reject: hippo quarantine reject <id>');
|
|
3949
|
+
}
|
|
3900
3950
|
/**
|
|
3901
3951
|
* `hippo tokens [--days <n>] [--json] [--global]`: the token ledger
|
|
3902
3952
|
* (ROADMAP TE0). Tokens of memory text handed to agents per surface, blocks
|
|
@@ -7979,6 +8029,9 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
7979
8029
|
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
7980
8030
|
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
7981
8031
|
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8032
|
+
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8033
|
+
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
8034
|
+
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
7982
8035
|
]);
|
|
7983
8036
|
function formatAuditRow(ev) {
|
|
7984
8037
|
const target = ev.targetId ?? '-';
|
|
@@ -8721,6 +8774,12 @@ Commands:
|
|
|
8721
8774
|
--global Operate on the global store
|
|
8722
8775
|
dormant restore <id> Bring a dormant memory back to active memory
|
|
8723
8776
|
dormant forget <id> Delete a dormant memory permanently
|
|
8777
|
+
quarantine [list] List memories a connector flagged as an instruction attempt, pending review
|
|
8778
|
+
--all Include approved and rejected rows too (default: pending only)
|
|
8779
|
+
--json Output as JSON
|
|
8780
|
+
--global Operate on the global store
|
|
8781
|
+
quarantine approve <id> Restore a quarantined memory to its original scope
|
|
8782
|
+
quarantine reject <id> Keep a quarantined memory hidden for good
|
|
8724
8783
|
capture-error Store a failed tool call as an error memory (reads the Claude Code
|
|
8725
8784
|
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
8726
8785
|
doctor Check the install: Node, store, schema, sleep, agent hooks
|
|
@@ -9478,6 +9537,9 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9478
9537
|
case 'dormant':
|
|
9479
9538
|
cmdDormant(hippoRoot, args, flags);
|
|
9480
9539
|
break;
|
|
9540
|
+
case 'quarantine':
|
|
9541
|
+
cmdQuarantine(hippoRoot, args, flags);
|
|
9542
|
+
break;
|
|
9481
9543
|
case 'tokens':
|
|
9482
9544
|
cmdTokens(hippoRoot, flags);
|
|
9483
9545
|
break;
|
|
@@ -108,6 +108,7 @@ export function ingestEvent(ctx, input) {
|
|
|
108
108
|
// v1.12.0: drop the legacy `|| 'connector:github'` fallback (see slack/ingest.ts:73 for rationale).
|
|
109
109
|
const result = remember(ctx, {
|
|
110
110
|
...opts,
|
|
111
|
+
untrusted: true,
|
|
111
112
|
afterWrite: (innerDb, memoryId) => {
|
|
112
113
|
if (input.__testInjectBeforeLog) {
|
|
113
114
|
input.__testInjectBeforeLog(innerDb, idempotencyKey);
|
|
@@ -61,6 +61,7 @@ export function ingestMessage(ctx, input) {
|
|
|
61
61
|
// ever missing); explicit reliance on the caller is safer.
|
|
62
62
|
const result = remember(ctx, {
|
|
63
63
|
...opts,
|
|
64
|
+
untrusted: true,
|
|
64
65
|
afterWrite: (innerDb, memoryId) => {
|
|
65
66
|
const inserted = innerDb
|
|
66
67
|
.prepare(`INSERT OR IGNORE INTO slack_event_log (event_id, ingested_at, memory_id) VALUES (?, ?, ?)`)
|
package/dist/consolidate.js
CHANGED
|
@@ -26,6 +26,7 @@ import { MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256 } from './memory-value-wei
|
|
|
26
26
|
import { appendAuditEvent } from './audit.js';
|
|
27
27
|
import { migrateDefaultHalfLife } from './half-life-migration.js';
|
|
28
28
|
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
29
|
+
import { isQuarantineScope } from './quarantine.js';
|
|
29
30
|
const DECAY_THRESHOLD = 0.05;
|
|
30
31
|
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
31
32
|
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
@@ -945,6 +946,8 @@ function detectConflicts(entries, now, decayOpts = {},
|
|
|
945
946
|
// Default empty set: flag-off behavior is unchanged.
|
|
946
947
|
rescuedIds = new Set()) {
|
|
947
948
|
const survivors = entries.filter((entry) => entry.layer !== Layer.Semantic
|
|
949
|
+
// CD5: an unreviewed quarantined row must not taint a visible memory as conflicted.
|
|
950
|
+
&& !isQuarantineScope(entry.scope ?? null)
|
|
948
951
|
&& (rescuedIds.has(entry.id) || calculateStrength(entry, now, decayOpts) >= DECAY_THRESHOLD));
|
|
949
952
|
const detected = [];
|
|
950
953
|
for (let i = 0; i < survivors.length; i++) {
|
package/dist/db.js
CHANGED
|
@@ -11,7 +11,7 @@ const require = createRequire(import.meta.url);
|
|
|
11
11
|
// runtime (Node's built-in synchronous SQLite module); there are no bundled
|
|
12
12
|
// types for it here, so this require + cast is the module's documented boundary.
|
|
13
13
|
const { DatabaseSync } = require('node:sqlite');
|
|
14
|
-
const CURRENT_SCHEMA_VERSION =
|
|
14
|
+
const CURRENT_SCHEMA_VERSION = 48;
|
|
15
15
|
const MIGRATIONS = [
|
|
16
16
|
{
|
|
17
17
|
version: 1,
|
|
@@ -2463,6 +2463,14 @@ const MIGRATIONS = [
|
|
|
2463
2463
|
`);
|
|
2464
2464
|
},
|
|
2465
2465
|
},
|
|
2466
|
+
{
|
|
2467
|
+
version: 48,
|
|
2468
|
+
up: (db) => {
|
|
2469
|
+
// Quarantine (src/quarantine.ts, CD5): a poisoned or suspect memory sits here pending
|
|
2470
|
+
// admin review instead of being hidden with no record. Additive only: no min_compatible_binary bump.
|
|
2471
|
+
db.exec(MEMORY_QUARANTINE_DDL);
|
|
2472
|
+
},
|
|
2473
|
+
},
|
|
2466
2474
|
];
|
|
2467
2475
|
function tableHasColumn(db, tableName, columnName) {
|
|
2468
2476
|
if (!/^[a-z_]+$/i.test(tableName))
|
|
@@ -2628,6 +2636,22 @@ function setSchemaVersion(db, version) {
|
|
|
2628
2636
|
db.prepare(`INSERT INTO meta(key, value) VALUES('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(String(version));
|
|
2629
2637
|
db.exec(`PRAGMA user_version = ${Math.max(0, Math.trunc(version))}`);
|
|
2630
2638
|
}
|
|
2639
|
+
// Shared by migration v48 and the stamped-store self-heal below.
|
|
2640
|
+
const MEMORY_QUARANTINE_DDL = `
|
|
2641
|
+
CREATE TABLE IF NOT EXISTS memory_quarantine (
|
|
2642
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2643
|
+
memory_id TEXT NOT NULL,
|
|
2644
|
+
original_scope TEXT,
|
|
2645
|
+
reason TEXT NOT NULL,
|
|
2646
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
|
|
2647
|
+
quarantined_at TEXT NOT NULL,
|
|
2648
|
+
decided_at TEXT,
|
|
2649
|
+
decided_by TEXT,
|
|
2650
|
+
PRIMARY KEY (tenant_id, memory_id)
|
|
2651
|
+
) WITHOUT ROWID;
|
|
2652
|
+
CREATE INDEX IF NOT EXISTS idx_memory_quarantine_status
|
|
2653
|
+
ON memory_quarantine(tenant_id, status, quarantined_at DESC);
|
|
2654
|
+
`;
|
|
2631
2655
|
// Before the loop on stamped stores: a table lost after its migration stamped (2026-08-15
|
|
2632
2656
|
// incident) is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
|
|
2633
2657
|
function ensureContinuityTables(db) {
|
|
@@ -2729,6 +2753,7 @@ function ensureContinuityTables(db) {
|
|
|
2729
2753
|
tenant_id TEXT NOT NULL DEFAULT 'default'
|
|
2730
2754
|
)
|
|
2731
2755
|
`);
|
|
2756
|
+
db.exec(MEMORY_QUARANTINE_DDL);
|
|
2732
2757
|
}
|
|
2733
2758
|
// After the loop: tenant_id (v16) and scope (v23) do not exist yet on a genuine old store.
|
|
2734
2759
|
function ensureContinuityIndexes(db) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Prompt-injection detection for untrusted memory content (CD5). Same shape as secret-detect.ts; leaf module, no store/api/shared imports. */
|
|
2
|
+
export interface InstructionDetection {
|
|
3
|
+
flagged: boolean;
|
|
4
|
+
reason: string | null;
|
|
5
|
+
}
|
|
6
|
+
export declare function detectInstruction(content: string): InstructionDetection;
|
|
7
|
+
//# sourceMappingURL=instruction-detect.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Prompt-injection detection for untrusted memory content (CD5). Same shape as secret-detect.ts; leaf module, no store/api/shared imports. */
|
|
2
|
+
const INSTRUCTION_PATTERNS = [
|
|
3
|
+
{ name: 'override-instructions', re: /\b(?:ignore|disregard|forget)\b[^.\n]{0,40}\b(?:previous|prior|above)\b[^.\n]{0,40}\binstructions?\b/i },
|
|
4
|
+
{ name: 'role-reassignment', re: /\b(?:you are now|act as|pretend to be)\b[^.\n]{0,40}\b(?:an?\s+)?(?:ai|assistant|agent|system)\b/i },
|
|
5
|
+
{ name: 'chat-role-markup', re: /<\|im_start\|>|<\|system\|>|\[INST\]|<\/?system>|###\s*system\s*:/i },
|
|
6
|
+
{ name: 'system-prompt-reference', re: /\b(?:(?:override|ignore|bypass|replace|reveal)\b[^.\n]{0,40}\b(?:system prompt|developer message)|(?:system prompt|developer message)\b[^.\n]{0,40}\b(?:override|ignore|bypass|replace|reveal))\b/i },
|
|
7
|
+
// "always"/"never" alone (ordinary PR prose) doesn't flag without an agent-facing target nearby too.
|
|
8
|
+
{ name: 'standing-order', re: /\b(?:from now on|whenever you|always|never)\b[^.\n]{0,50}\b(?:you must|(?:the ai|the assistant|the agent|claude|copilot)\s+(?:must|should|will)\b)/i },
|
|
9
|
+
{ name: 'concealment', re: /\b(?:do not|never|don't)\b[^.\n]{0,40}\b(?:tell|mention|reveal)\b[^.\n]{0,40}\bthe user\b/i },
|
|
10
|
+
{ name: 'remote-script-exec', re: /\b(?:curl|wget)\b[^\n|]{0,80}\|\s*(?:sudo\s+)?(?:sh|bash|zsh)\b|\biex\s*\(\s*(?:iwr|invoke-webrequest|new-object\s+net\.webclient)/i },
|
|
11
|
+
{ name: 'exfiltration', re: /\b(?:send|post|upload|exfiltrate)\b[^.\n]{0,40}\b(?:secrets?|tokens?|api[- ]?keys?|credentials?|env(?:ironment)? variables?)\b/i },
|
|
12
|
+
{ name: 'unicode-tag-chars', re: /[\u{E0000}-\u{E007F}]/u },
|
|
13
|
+
];
|
|
14
|
+
// A stray zero-width char has legitimate uses (ZWJ); only a run of 3+ adjacent ones reads as smuggling.
|
|
15
|
+
const ZERO_WIDTH_RUN_RE = /[\u200B-\u200F\u2060-\u2064]{3,}/;
|
|
16
|
+
export function detectInstruction(content) {
|
|
17
|
+
for (const { name, re } of INSTRUCTION_PATTERNS) {
|
|
18
|
+
if (re.test(content)) {
|
|
19
|
+
return { flagged: true, reason: `pattern:${name}` };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (ZERO_WIDTH_RUN_RE.test(content)) {
|
|
23
|
+
return { flagged: true, reason: 'pattern:zero-width-smuggling' };
|
|
24
|
+
}
|
|
25
|
+
return { flagged: false, reason: null };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=instruction-detect.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Quarantine tier (CD5, AT3): pending/approved/rejected record for a memory `remember` flagged as untrusted. */
|
|
2
|
+
import type { DatabaseSyncLike } from './db.js';
|
|
3
|
+
export declare const QUARANTINE_SCOPE_PREFIX = "quarantine:private:";
|
|
4
|
+
/** Scope a quarantined memory is stored under; matches PRIVATE_SCOPE_RE so every default-deny site hides it. */
|
|
5
|
+
export declare function quarantineScopeFor(original: string | null): string;
|
|
6
|
+
export declare function isQuarantineScope(scope: string | null | undefined): boolean;
|
|
7
|
+
export type QuarantineStatus = 'pending' | 'approved' | 'rejected';
|
|
8
|
+
export interface QuarantineRow {
|
|
9
|
+
tenantId: string;
|
|
10
|
+
memoryId: string;
|
|
11
|
+
originalScope: string | null;
|
|
12
|
+
reason: string;
|
|
13
|
+
status: QuarantineStatus;
|
|
14
|
+
quarantinedAt: string;
|
|
15
|
+
decidedAt: string | null;
|
|
16
|
+
decidedBy: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface RecordQuarantineOpts {
|
|
19
|
+
tenantId: string;
|
|
20
|
+
memoryId: string;
|
|
21
|
+
originalScope: string | null;
|
|
22
|
+
reason: string;
|
|
23
|
+
actor: string;
|
|
24
|
+
}
|
|
25
|
+
/** Insert the quarantine row + its audit event; caller runs this inside the memory's own write transaction. */
|
|
26
|
+
export declare function recordQuarantine(db: DatabaseSyncLike, opts: RecordQuarantineOpts): void;
|
|
27
|
+
export declare function getQuarantineRow(db: DatabaseSyncLike, tenantId: string, memoryId: string): QuarantineRow | null;
|
|
28
|
+
export declare function listQuarantineRows(db: DatabaseSyncLike, tenantId: string, status?: QuarantineStatus | 'all', limit?: number): QuarantineRow[];
|
|
29
|
+
export declare function approveQuarantineRow(db: DatabaseSyncLike, tenantId: string, memoryId: string, decidedBy: string): void;
|
|
30
|
+
export declare function rejectQuarantineRow(db: DatabaseSyncLike, tenantId: string, memoryId: string, decidedBy: string): void;
|
|
31
|
+
//# sourceMappingURL=quarantine.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Quarantine tier (CD5, AT3): pending/approved/rejected record for a memory `remember` flagged as untrusted. */
|
|
2
|
+
import { appendAuditEvent } from './audit.js';
|
|
3
|
+
export const QUARANTINE_SCOPE_PREFIX = 'quarantine:private:';
|
|
4
|
+
/** Scope a quarantined memory is stored under; matches PRIVATE_SCOPE_RE so every default-deny site hides it. */
|
|
5
|
+
export function quarantineScopeFor(original) {
|
|
6
|
+
return `${QUARANTINE_SCOPE_PREFIX}${original ?? 'unscoped'}`;
|
|
7
|
+
}
|
|
8
|
+
export function isQuarantineScope(scope) {
|
|
9
|
+
return scope != null && scope.startsWith(QUARANTINE_SCOPE_PREFIX);
|
|
10
|
+
}
|
|
11
|
+
const SELECT_COLUMNS = 'tenant_id, memory_id, original_scope, reason, status, quarantined_at, decided_at, decided_by';
|
|
12
|
+
function fromDbRow(row) {
|
|
13
|
+
return {
|
|
14
|
+
tenantId: row.tenant_id,
|
|
15
|
+
memoryId: row.memory_id,
|
|
16
|
+
originalScope: row.original_scope,
|
|
17
|
+
reason: row.reason,
|
|
18
|
+
// SAFETY: this module's own INSERT/UPDATE statements are the only writers of status.
|
|
19
|
+
status: row.status,
|
|
20
|
+
quarantinedAt: row.quarantined_at,
|
|
21
|
+
decidedAt: row.decided_at,
|
|
22
|
+
decidedBy: row.decided_by,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Insert the quarantine row + its audit event; caller runs this inside the memory's own write transaction. */
|
|
26
|
+
export function recordQuarantine(db, opts) {
|
|
27
|
+
db.prepare(`INSERT INTO memory_quarantine (tenant_id, memory_id, original_scope, reason, status, quarantined_at)
|
|
28
|
+
VALUES (?, ?, ?, ?, 'pending', ?)`).run(opts.tenantId, opts.memoryId, opts.originalScope, opts.reason, new Date().toISOString());
|
|
29
|
+
appendAuditEvent(db, {
|
|
30
|
+
tenantId: opts.tenantId,
|
|
31
|
+
actor: opts.actor,
|
|
32
|
+
op: 'quarantine',
|
|
33
|
+
targetId: opts.memoryId,
|
|
34
|
+
metadata: { reason: opts.reason, originalScope: opts.originalScope },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function getQuarantineRow(db, tenantId, memoryId) {
|
|
38
|
+
const row = db.prepare(`SELECT ${SELECT_COLUMNS} FROM memory_quarantine WHERE tenant_id = ? AND memory_id = ?`)
|
|
39
|
+
.get(tenantId, memoryId);
|
|
40
|
+
return row ? fromDbRow(row) : null;
|
|
41
|
+
}
|
|
42
|
+
export function listQuarantineRows(db, tenantId, status = 'pending', limit = 100) {
|
|
43
|
+
// A pending row whose memory was deleted (e.g. Slack message_deleted) is dead; keep its history, drop it from the queue.
|
|
44
|
+
const live = status === 'pending'
|
|
45
|
+
? ' AND EXISTS (SELECT 1 FROM memories m WHERE m.id = memory_quarantine.memory_id AND m.tenant_id = memory_quarantine.tenant_id)'
|
|
46
|
+
: '';
|
|
47
|
+
const rows = status === 'all'
|
|
48
|
+
? db.prepare(`SELECT ${SELECT_COLUMNS} FROM memory_quarantine WHERE tenant_id = ? ORDER BY quarantined_at DESC LIMIT ?`)
|
|
49
|
+
.all(tenantId, limit)
|
|
50
|
+
: db.prepare(`SELECT ${SELECT_COLUMNS} FROM memory_quarantine WHERE tenant_id = ? AND status = ?${live} ORDER BY quarantined_at DESC LIMIT ?`)
|
|
51
|
+
.all(tenantId, status, limit);
|
|
52
|
+
// SAFETY: both branches select SELECT_COLUMNS, matching QuarantineDbRow's field set.
|
|
53
|
+
return rows.map(fromDbRow);
|
|
54
|
+
}
|
|
55
|
+
export function approveQuarantineRow(db, tenantId, memoryId, decidedBy) {
|
|
56
|
+
db.prepare(`UPDATE memory_quarantine SET status = 'approved', decided_at = ?, decided_by = ? WHERE tenant_id = ? AND memory_id = ?`)
|
|
57
|
+
.run(new Date().toISOString(), decidedBy, tenantId, memoryId);
|
|
58
|
+
}
|
|
59
|
+
export function rejectQuarantineRow(db, tenantId, memoryId, decidedBy) {
|
|
60
|
+
db.prepare(`UPDATE memory_quarantine SET status = 'rejected', decided_at = ?, decided_by = ? WHERE tenant_id = ? AND memory_id = ?`)
|
|
61
|
+
.run(new Date().toISOString(), decidedBy, tenantId, memoryId);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=quarantine.js.map
|
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';
|
|
@@ -118,6 +118,9 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
118
118
|
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
119
119
|
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
120
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
|
|
121
124
|
]);
|
|
122
125
|
// Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
|
|
123
126
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
@@ -1163,6 +1166,61 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1163
1166
|
sendJson(res, 200, result);
|
|
1164
1167
|
return;
|
|
1165
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
|
+
}
|
|
1166
1224
|
// GET /v1/audit?op=&since=&limit= — read audit events. All three filters
|
|
1167
1225
|
// validated at the route boundary so an invalid value lands a 400 before
|
|
1168
1226
|
// we hit the DB.
|
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",
|