hippo-memory 1.53.2 → 1.54.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 +4 -2
- package/dist/api.js +14 -10
- package/dist/audit.d.ts +19 -0
- package/dist/audit.js +40 -6
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +3 -2
- package/dist/cli.js +3 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +6 -7
- package/dist/server.d.ts +14 -0
- package/dist/server.js +227 -128
- 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 +3 -2
package/dist/api.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface Actor {
|
|
|
43
43
|
role: 'admin' | 'member';
|
|
44
44
|
/** EI2: restricted scopes a member key may read (auth.ts grantScope). Unused for admin actors. */
|
|
45
45
|
scopes?: readonly string[];
|
|
46
|
+
/** An auth resolver vouched for this caller, so its admin role stops at its own tenant. */
|
|
47
|
+
viaAuthResolver?: true;
|
|
46
48
|
}
|
|
47
49
|
export interface Context {
|
|
48
50
|
hippoRoot: string;
|
|
@@ -869,8 +871,8 @@ export interface AuthCreateResult {
|
|
|
869
871
|
* `src/server.ts` POST /v1/auth/keys mirrors this: it ignores any body
|
|
870
872
|
* `tenantId` and uses the resolved Bearer's tenant exclusively.
|
|
871
873
|
*
|
|
872
|
-
* Only an admin actor can mint (ForbiddenError otherwise),
|
|
873
|
-
*
|
|
874
|
+
* Only an admin actor can mint (ForbiddenError otherwise), and a key never
|
|
875
|
+
* outranks its minter: a resolver admin is tenant-only, so it mints members.
|
|
874
876
|
*/
|
|
875
877
|
export declare function authCreate(ctx: Context, opts: AuthCreateOpts): AuthCreateResult;
|
|
876
878
|
/**
|
package/dist/api.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* (`hippo serve`, A1) call into this module so the business logic lives
|
|
7
7
|
* in exactly one place.
|
|
8
8
|
*/
|
|
9
|
-
import { createHash } from 'node:crypto';
|
|
10
9
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
11
10
|
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, memoriesBackingObjects, } from './store.js';
|
|
12
11
|
import { RejectedValueError } from './rejection.js';
|
|
@@ -18,7 +17,7 @@ import { quarantineScopeFor, recordQuarantine, getQuarantineRow, listQuarantineR
|
|
|
18
17
|
import { summarizeFailures } from './failure-log.js';
|
|
19
18
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
20
19
|
import { createMemory, createSuccessor, applyOutcome, calculateStrength, CHURN_STALE_TAG, COMPACTION_MEMORY_TAG, } from './memory.js';
|
|
21
|
-
import { appendAuditEvent, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
20
|
+
import { appendAuditEvent, auditQueryFields, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
22
21
|
import { promoteToGlobal, getGlobalRoot, autoShare, searchBothHybrid } from './shared.js';
|
|
23
22
|
import { writeRecallTrace, writeRecallTraceAtRoot, recordTraceOutcome } from './recall-trace.js';
|
|
24
23
|
import { evalNow } from './ablation.js';
|
|
@@ -549,8 +548,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
549
548
|
actor: ctx.actor.subject,
|
|
550
549
|
op: 'recall',
|
|
551
550
|
metadata: {
|
|
552
|
-
|
|
553
|
-
query_length: opts.query.length,
|
|
551
|
+
...auditQueryFields(opts.query),
|
|
554
552
|
results: rankedOut.length,
|
|
555
553
|
},
|
|
556
554
|
});
|
|
@@ -1348,16 +1346,19 @@ export function archiveRaw(ctx, id, reason, opts = {}) {
|
|
|
1348
1346
|
* `src/server.ts` POST /v1/auth/keys mirrors this: it ignores any body
|
|
1349
1347
|
* `tenantId` and uses the resolved Bearer's tenant exclusively.
|
|
1350
1348
|
*
|
|
1351
|
-
* Only an admin actor can mint (ForbiddenError otherwise),
|
|
1352
|
-
*
|
|
1349
|
+
* Only an admin actor can mint (ForbiddenError otherwise), and a key never
|
|
1350
|
+
* outranks its minter: a resolver admin is tenant-only, so it mints members.
|
|
1353
1351
|
*/
|
|
1354
1352
|
export function authCreate(ctx, opts) {
|
|
1355
1353
|
if (ctx.actor.role !== 'admin') {
|
|
1356
1354
|
throw new ForbiddenError('Only an admin key can create API keys');
|
|
1357
1355
|
}
|
|
1356
|
+
if (ctx.actor.viaAuthResolver && opts.role === 'admin') {
|
|
1357
|
+
throw new ForbiddenError('A key minted through the auth resolver can only be a member key');
|
|
1358
|
+
}
|
|
1358
1359
|
const db = openHippoDb(ctx.hippoRoot);
|
|
1359
1360
|
try {
|
|
1360
|
-
const role = opts.role ?? 'admin';
|
|
1361
|
+
const role = opts.role ?? (ctx.actor.viaAuthResolver ? 'member' : 'admin');
|
|
1361
1362
|
const result = createApiKey(db, { tenantId: ctx.tenantId, label: opts.label, role });
|
|
1362
1363
|
// v1.12.4: audit emit (closes the gap v1.12.3 CHANGELOG flagged as deferred).
|
|
1363
1364
|
// Mirrors the auth_revoke pattern at authRevoke — same try/catch so audit
|
|
@@ -1408,10 +1409,10 @@ export function authRevoke(ctx, keyId) {
|
|
|
1408
1409
|
}
|
|
1409
1410
|
const db = openHippoDb(ctx.hippoRoot);
|
|
1410
1411
|
try {
|
|
1411
|
-
// SAFETY: row's shape matches the
|
|
1412
|
+
// SAFETY: row's shape matches the four columns named in the SELECT
|
|
1412
1413
|
// above.
|
|
1413
1414
|
const row = db
|
|
1414
|
-
.prepare(`SELECT key_id, tenant_id, revoked_at FROM api_keys WHERE key_id = ?`)
|
|
1415
|
+
.prepare(`SELECT key_id, tenant_id, revoked_at, role FROM api_keys WHERE key_id = ?`)
|
|
1415
1416
|
.get(keyId);
|
|
1416
1417
|
if (!row) {
|
|
1417
1418
|
throw new Error(`Unknown key_id: ${keyId}`);
|
|
@@ -1420,6 +1421,9 @@ export function authRevoke(ctx, keyId) {
|
|
|
1420
1421
|
if (row.tenant_id !== ctx.tenantId) {
|
|
1421
1422
|
throw new Error(`Unknown key_id: ${keyId}`);
|
|
1422
1423
|
}
|
|
1424
|
+
if (ctx.actor.viaAuthResolver && row.role === 'admin') {
|
|
1425
|
+
throw new ForbiddenError('An auth resolver admin cannot revoke an admin key, which outranks it');
|
|
1426
|
+
}
|
|
1423
1427
|
let revokedAt;
|
|
1424
1428
|
let alreadyRevoked = false;
|
|
1425
1429
|
if (row.revoked_at) {
|
|
@@ -1930,7 +1934,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1930
1934
|
// 'recall' op emitted by api.recall for parity). pinnedOnly + '*' fallback
|
|
1931
1935
|
// never hit the search engines, so they don't emit (matches cmdContext).
|
|
1932
1936
|
const ctxRecallMetadata = {
|
|
1933
|
-
query
|
|
1937
|
+
...auditQueryFields(query),
|
|
1934
1938
|
results: selectedItems.length,
|
|
1935
1939
|
mode: 'context',
|
|
1936
1940
|
};
|
package/dist/audit.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface AppendAuditOpts {
|
|
|
27
27
|
targetId?: string;
|
|
28
28
|
metadata?: unknown;
|
|
29
29
|
}
|
|
30
|
+
export type AuditQueryFields = {
|
|
31
|
+
query_hash: string;
|
|
32
|
+
query_length: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function auditQueryFields(query: string): AuditQueryFields;
|
|
30
35
|
export declare function appendAuditEvent(db: DatabaseSyncLike, opts: AppendAuditOpts): void;
|
|
31
36
|
export interface QueryAuditOpts {
|
|
32
37
|
tenantId: string;
|
|
@@ -44,4 +49,18 @@ export interface AuditEvent {
|
|
|
44
49
|
metadata: JsonObject;
|
|
45
50
|
}
|
|
46
51
|
export declare function queryAuditEvents(db: DatabaseSyncLike, opts: QueryAuditOpts): AuditEvent[];
|
|
52
|
+
export interface ListAuditAfterOpts {
|
|
53
|
+
/** Last id already consumed; 0 starts from the beginning. */
|
|
54
|
+
afterId: number;
|
|
55
|
+
/** Clamped to 1..10000; default 1000. */
|
|
56
|
+
limit?: number;
|
|
57
|
+
/** Omit for every tenant (deployment-wide export). */
|
|
58
|
+
tenantId?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Cursor read: events with id > afterId, ascending by id. Ids are AUTOINCREMENT
|
|
62
|
+
* and never reused, but deletes (retention prune) leave gaps, so resume from
|
|
63
|
+
* the last id returned, never from a count.
|
|
64
|
+
*/
|
|
65
|
+
export declare function listAuditEventsAfter(db: DatabaseSyncLike, opts: ListAuditAfterOpts): AuditEvent[];
|
|
47
66
|
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { canAutoDelete } from './memory.js';
|
|
2
3
|
export const STOP_WORDS = new Set([
|
|
3
4
|
'the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being',
|
|
@@ -240,6 +241,12 @@ function isBigIntValue(value) {
|
|
|
240
241
|
function bigintSafeReplacer(_key, value) {
|
|
241
242
|
return isBigIntValue(value) ? value.toString() : value;
|
|
242
243
|
}
|
|
244
|
+
export function auditQueryFields(query) {
|
|
245
|
+
return {
|
|
246
|
+
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
247
|
+
query_length: query.length,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
243
250
|
export function appendAuditEvent(db, opts) {
|
|
244
251
|
db.prepare(`INSERT INTO audit_log (ts, tenant_id, actor, op, target_id, metadata_json) VALUES (?, ?, ?, ?, ?, ?)`).run(new Date().toISOString(), opts.tenantId, opts.actor, opts.op, opts.targetId ?? null, JSON.stringify(opts.metadata ?? {}, bigintSafeReplacer));
|
|
245
252
|
}
|
|
@@ -255,13 +262,40 @@ export function queryAuditEvents(db, opts) {
|
|
|
255
262
|
params.push(opts.since);
|
|
256
263
|
}
|
|
257
264
|
const limit = Math.max(1, Math.min(opts.limit ?? 100, 10000));
|
|
258
|
-
// SAFETY:
|
|
259
|
-
|
|
265
|
+
// SAFETY: AUDIT_COLUMNS names exactly the AuditRow columns, in this order.
|
|
266
|
+
const rows = db
|
|
267
|
+
.prepare(`SELECT ${AUDIT_COLUMNS} FROM audit_log WHERE ${where.join(' AND ')} ORDER BY ts DESC, id DESC LIMIT ?`)
|
|
268
|
+
.all(...params, limit);
|
|
269
|
+
return rows.map(rowToAuditEvent);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Cursor read: events with id > afterId, ascending by id. Ids are AUTOINCREMENT
|
|
273
|
+
* and never reused, but deletes (retention prune) leave gaps, so resume from
|
|
274
|
+
* the last id returned, never from a count.
|
|
275
|
+
*/
|
|
276
|
+
export function listAuditEventsAfter(db, opts) {
|
|
277
|
+
if (!Number.isInteger(opts.afterId) || opts.afterId < 0) {
|
|
278
|
+
throw new RangeError('afterId must be a non-negative integer');
|
|
279
|
+
}
|
|
280
|
+
if (opts.limit !== undefined && !Number.isInteger(opts.limit)) {
|
|
281
|
+
throw new RangeError('limit must be an integer');
|
|
282
|
+
}
|
|
283
|
+
const where = ['id > ?'];
|
|
284
|
+
const params = [opts.afterId];
|
|
285
|
+
if (opts.tenantId !== undefined) {
|
|
286
|
+
where.push('+tenant_id = ?');
|
|
287
|
+
params.push(opts.tenantId);
|
|
288
|
+
}
|
|
289
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 1000, 10000));
|
|
290
|
+
// SAFETY: AUDIT_COLUMNS names exactly the AuditRow columns, in this order.
|
|
260
291
|
const rows = db
|
|
261
|
-
.prepare(`SELECT
|
|
262
|
-
FROM audit_log WHERE ${where.join(' AND ')} ORDER BY ts DESC, id DESC LIMIT ?`)
|
|
292
|
+
.prepare(`SELECT ${AUDIT_COLUMNS} FROM audit_log WHERE ${where.join(' AND ')} ORDER BY id ASC LIMIT ?`)
|
|
263
293
|
.all(...params, limit);
|
|
264
|
-
return rows.map(
|
|
294
|
+
return rows.map(rowToAuditEvent);
|
|
295
|
+
}
|
|
296
|
+
const AUDIT_COLUMNS = 'id, ts, tenant_id, actor, op, target_id, metadata_json';
|
|
297
|
+
function rowToAuditEvent(r) {
|
|
298
|
+
return {
|
|
265
299
|
id: r.id,
|
|
266
300
|
ts: r.ts,
|
|
267
301
|
tenantId: r.tenant_id,
|
|
@@ -271,7 +305,7 @@ export function queryAuditEvents(db, opts) {
|
|
|
271
305
|
op: r.op,
|
|
272
306
|
targetId: r.target_id,
|
|
273
307
|
metadata: safeJsonParse(r.metadata_json),
|
|
274
|
-
}
|
|
308
|
+
};
|
|
275
309
|
}
|
|
276
310
|
function safeJsonParse(raw) {
|
|
277
311
|
try {
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DatabaseSyncLike } from './db.js';
|
|
2
|
+
/** Every minted API key starts with this, so the server can route a bearer token by shape. */
|
|
3
|
+
export declare const API_KEY_PREFIX = "hk_";
|
|
2
4
|
export declare function _dummyHashForTests(): string;
|
|
3
5
|
export interface CreateApiKeyOpts {
|
|
4
6
|
tenantId: string;
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
|
|
2
|
+
/** Every minted API key starts with this, so the server can route a bearer token by shape. */
|
|
3
|
+
export const API_KEY_PREFIX = 'hk_';
|
|
3
4
|
const ID_LEN = 24; // base32 chars after prefix
|
|
4
5
|
const SECRET_LEN = 32; // base32 chars after dot
|
|
5
6
|
const SCRYPT_KEYLEN = 32;
|
|
@@ -40,7 +41,7 @@ function verifyKey(plaintext, stored) {
|
|
|
40
41
|
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
41
42
|
}
|
|
42
43
|
export function createApiKey(db, opts) {
|
|
43
|
-
const keyId = `${
|
|
44
|
+
const keyId = `${API_KEY_PREFIX}${randBase32(ID_LEN)}`;
|
|
44
45
|
const secret = randBase32(SECRET_LEN);
|
|
45
46
|
const plaintext = `${keyId}.${secret}`;
|
|
46
47
|
const hash = hashKey(plaintext);
|
package/dist/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ import { importChatGPT, importClaude, importCursor, importGenericFile, importMar
|
|
|
81
81
|
import { cmdCapture, cmdPreCompact, cmdPostCompact, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage, transcriptWorkingState } from './capture.js';
|
|
82
82
|
import { COMPACTION_DB_WAIT_MS, replayCompactionsAt } from './compaction-record.js';
|
|
83
83
|
import { readStdinBounded } from './stdin.js';
|
|
84
|
-
import { auditMemories, appendAuditEvent, AUDIT_OPS, } from './audit.js';
|
|
84
|
+
import { auditMemories, appendAuditEvent, auditQueryFields, AUDIT_OPS, } from './audit.js';
|
|
85
85
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
86
86
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
87
87
|
import { buildCorrectionLatency } from './correction-latency.js';
|
|
@@ -1853,10 +1853,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1853
1853
|
}
|
|
1854
1854
|
else if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
1855
1855
|
// SHA-256/16 per the recall-audit convention; hashQueryText is FNV-1a and brute-forceable on short queries.
|
|
1856
|
-
emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined,
|
|
1857
|
-
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
1858
|
-
query_length: query.length,
|
|
1859
|
-
});
|
|
1856
|
+
emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined, auditQueryFields(query));
|
|
1860
1857
|
}
|
|
1861
1858
|
if (cmdAnchoringHint?.reason === 'memory_dominance') {
|
|
1862
1859
|
emitCliAudit(hippoRoot, 'recall_anchor_detected_memory_dominance', cmdAnchoringHint.memoryId, {
|
|
@@ -1878,7 +1875,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1878
1875
|
}
|
|
1879
1876
|
// A5 audit: one 'recall' event per query, before the early-empty return, in every participating store.
|
|
1880
1877
|
const recallMetadata = {
|
|
1881
|
-
query
|
|
1878
|
+
...auditQueryFields(query),
|
|
1882
1879
|
results: results.length,
|
|
1883
1880
|
};
|
|
1884
1881
|
emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
|
package/dist/index.d.ts
CHANGED
|
@@ -21,4 +21,6 @@ export { importChatGPT, importClaude, importCursor, importGenericFile, importMar
|
|
|
21
21
|
export { runFeatureEval, formatResult, resultToBaseline, detectRegressions, buildSyntheticCorpus, } from './eval-suite.js';
|
|
22
22
|
export { computeSalience, SalienceDecision, SalienceResult, SalienceOptions, } from './salience.js';
|
|
23
23
|
export { computeAmbientState, renderAmbientSummary, formatAmbientVector, AmbientState, } from './ambient.js';
|
|
24
|
+
export { appendAuditEvent, queryAuditEvents, listAuditEventsAfter, AUDIT_OPS, type AuditEvent, type AuditOp, type QueryAuditOpts, type ListAuditAfterOpts, } from './audit.js';
|
|
25
|
+
export { openHippoDb, openHippoDbReadOnly, closeHippoDb, type DatabaseSyncLike } from './db.js';
|
|
24
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -34,4 +34,6 @@ export { runFeatureEval, formatResult, resultToBaseline, detectRegressions, buil
|
|
|
34
34
|
export { computeSalience, } from './salience.js';
|
|
35
35
|
// Pineal gland: ambient state vector
|
|
36
36
|
export { computeAmbientState, renderAmbientSummary, formatAmbientVector, } from './ambient.js';
|
|
37
|
+
export { appendAuditEvent, queryAuditEvents, listAuditEventsAfter, AUDIT_OPS, } from './audit.js';
|
|
38
|
+
export { openHippoDb, openHippoDbReadOnly, closeHippoDb } from './db.js';
|
|
37
39
|
//# sourceMappingURL=index.js.map
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export interface McpContext {
|
|
|
50
50
|
role?: 'admin' | 'member';
|
|
51
51
|
/** EI2: scope grants for the HTTP-MCP caller's key. Absent for stdio (admin, needs none). */
|
|
52
52
|
scopes?: readonly string[];
|
|
53
|
+
viaAuthResolver?: true;
|
|
53
54
|
/**
|
|
54
55
|
* Per-client key for state isolation under HTTP-MCP. For stdio: 'stdio-${pid}'
|
|
55
56
|
* (one process = one client). For HTTP-SSE / HTTP MCP: hash(bearer + remoteAddr)
|
package/dist/mcp/server.js
CHANGED
|
@@ -25,9 +25,8 @@ import { recall as apiRecall, remember as apiRemember, outcome as apiOutcome, dr
|
|
|
25
25
|
import { assertScopeRequestAllowed } from '../recall-scope.js';
|
|
26
26
|
import { resolveProjectIdentity, classifyOriginProject, findHippoStoreDir } from '../project-identity.js';
|
|
27
27
|
import { computePredictionBaserate } from '../predictions.js';
|
|
28
|
-
import { appendAuditEvent } from '../audit.js';
|
|
28
|
+
import { appendAuditEvent, auditQueryFields } from '../audit.js';
|
|
29
29
|
import { RejectedValueError } from '../rejection.js';
|
|
30
|
-
import { createHash } from 'node:crypto';
|
|
31
30
|
import { detectAnchoring, hashQueryText, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
|
|
32
31
|
import { detectAvailabilityBias } from '../availability.js';
|
|
33
32
|
// v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
|
|
@@ -58,7 +57,10 @@ export function findHippoRoot(cwd = process.cwd(), opts) {
|
|
|
58
57
|
* a member key never acts as admin through MCP.
|
|
59
58
|
*/
|
|
60
59
|
function mcpActor(ctx) {
|
|
61
|
-
|
|
60
|
+
const actor = { subject: ctx?.actor ?? 'mcp', role: ctx?.role ?? 'admin', scopes: ctx?.scopes };
|
|
61
|
+
if (ctx?.viaAuthResolver)
|
|
62
|
+
actor.viaAuthResolver = true;
|
|
63
|
+
return actor;
|
|
62
64
|
}
|
|
63
65
|
// MCP stdio transport spec: messages are newline-delimited JSON-RPC, no embedded newlines.
|
|
64
66
|
// https://modelcontextprotocol.io/specification/.../basic/transports#stdio
|
|
@@ -738,10 +740,7 @@ async function executeTool(name, args, ctx) {
|
|
|
738
740
|
actor: ctx?.actor ?? 'mcp',
|
|
739
741
|
op: 'recall_anchor_skipped_no_session',
|
|
740
742
|
targetId: undefined,
|
|
741
|
-
metadata:
|
|
742
|
-
query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
|
|
743
|
-
query_length: query.length,
|
|
744
|
-
},
|
|
743
|
+
metadata: auditQueryFields(query),
|
|
745
744
|
});
|
|
746
745
|
}
|
|
747
746
|
finally {
|
package/dist/server.d.ts
CHANGED
|
@@ -11,8 +11,22 @@ export interface ServerHandle {
|
|
|
11
11
|
* flow outside tests. */
|
|
12
12
|
server?: import('node:http').Server;
|
|
13
13
|
}
|
|
14
|
+
/** Identity an {@link AuthResolver} vouches for. The core sanitises it before use. */
|
|
15
|
+
export interface ResolvedBearer {
|
|
16
|
+
tenantId: string;
|
|
17
|
+
subject: string;
|
|
18
|
+
/** Not 'admin' means 'member'. Admin is tenant-only, yet can mint member API keys (POST /v1/auth/keys) that outlive IdP deprovisioning. */
|
|
19
|
+
role: 'admin' | 'member';
|
|
20
|
+
scopes?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/** Sole judge of non-`hk_` bearer tokens: null is a 401; a throw or missed deadline is a 503, so throw only when upstream is down. */
|
|
23
|
+
export type AuthResolver = (token: string) => ResolvedBearer | null | Promise<ResolvedBearer | null>;
|
|
14
24
|
export interface ServeOpts {
|
|
15
25
|
hippoRoot: string;
|
|
26
|
+
/** Runs on every request and SSE heartbeat, so keep it cache-backed; API keys never reach it. */
|
|
27
|
+
authResolver?: AuthResolver;
|
|
28
|
+
/** Deadline for one authResolver call; defaults to 5000 ms. */
|
|
29
|
+
authResolverTimeoutMs?: number;
|
|
16
30
|
port?: number;
|
|
17
31
|
host?: string;
|
|
18
32
|
/** Stop and exit on SIGINT/SIGTERM. Only `hippo serve` owns the process, so only it sets this. */
|