hippo-memory 1.63.2 → 1.64.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 +10 -4
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- 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 +8 -4
package/dist/server/mcp-http.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// MCP over HTTP: POST /mcp and the GET /mcp/stream SSE keepalive.
|
|
2
2
|
import { envMcpSseHeartbeatMs, envMcpSseMaxAgeSec, envMcpSseMaxStreams } from '../env.js';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isSharedStore } from '../config.js';
|
|
4
5
|
import { handleMcpRequest, mcpErrorResponse } from '../mcp/server.js';
|
|
5
6
|
import { HttpError, isJsonObjectRecord, readBody, sendJson } from '../http-util.js';
|
|
7
|
+
import { assertCallerProject } from '../project-identity.js';
|
|
6
8
|
import { buildContextWithAuth, heartbeatVerdict, readAuthHeader, requireAuth } from './auth.js';
|
|
7
9
|
import { clientIpForRateLimit } from './client-ip.js';
|
|
8
10
|
import { requestIds } from './request.js';
|
|
@@ -44,6 +46,57 @@ function buildMcpClientKey(req) {
|
|
|
44
46
|
//
|
|
45
47
|
// Auth: same as /v1/* — Bearer token validated via `requireAuth`, with the
|
|
46
48
|
// loopback no-auth fallback. SSE check runs once at stream-open.
|
|
49
|
+
// Percent-encoded by the client, so any lowercase Unicode name fits a Latin-1 header.
|
|
50
|
+
const HEADER_PIECE = /^[A-Za-z0-9\-_.!~*'()%]+$/;
|
|
51
|
+
function decodeHeaderPiece(raw, header) {
|
|
52
|
+
if (!HEADER_PIECE.test(raw))
|
|
53
|
+
throw new HttpError(400, `${header} must be percent-encoded, with no empty names`);
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(raw);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new HttpError(400, `${header} holds a bad percent escape`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** On a shared store, the caller's project from X-Hippo-Project and the comma-separated X-Hippo-Project-Aliases; any other store ignores both. */
|
|
62
|
+
export function callerProjectFromHeaders(req, hippoRoot) {
|
|
63
|
+
if (!isSharedStore(hippoRoot))
|
|
64
|
+
return undefined;
|
|
65
|
+
const names = req.headersDistinct['x-hippo-project'];
|
|
66
|
+
const aliasHeaders = req.headersDistinct['x-hippo-project-aliases'];
|
|
67
|
+
if (names === undefined) {
|
|
68
|
+
if (aliasHeaders !== undefined)
|
|
69
|
+
throw new HttpError(400, 'X-Hippo-Project-Aliases needs X-Hippo-Project');
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (names.length > 1 || (aliasHeaders?.length ?? 0) > 1)
|
|
73
|
+
throw new HttpError(400, 'send X-Hippo-Project and X-Hippo-Project-Aliases once each');
|
|
74
|
+
const name = decodeHeaderPiece(names[0], 'X-Hippo-Project');
|
|
75
|
+
const rawAliases = aliasHeaders?.[0] ?? '';
|
|
76
|
+
const aliases = rawAliases === '' ? [] : rawAliases.split(',').map((a) => decodeHeaderPiece(a, 'X-Hippo-Project-Aliases'));
|
|
77
|
+
assertCallerProject({ name, aliases });
|
|
78
|
+
return { name, legacyName: name, aliases };
|
|
79
|
+
}
|
|
80
|
+
export function mcpContextFor(ctx, clientKey, autoSleep, project) {
|
|
81
|
+
const mcpCtx = {
|
|
82
|
+
hippoRoot: ctx.hippoRoot,
|
|
83
|
+
tenantId: ctx.tenantId,
|
|
84
|
+
// McpContext.actor stays string; extract subject at the boundary.
|
|
85
|
+
actor: ctx.actor.subject,
|
|
86
|
+
// The caller's real role: MCP tools must not run a member key as admin.
|
|
87
|
+
role: ctx.actor.role,
|
|
88
|
+
scopes: ctx.actor.scopes,
|
|
89
|
+
viaAuthResolver: ctx.actor.viaAuthResolver,
|
|
90
|
+
hostAdmin: ctx.actor.hostAdmin,
|
|
91
|
+
owner: ctx.actor.owner,
|
|
92
|
+
clientKey,
|
|
93
|
+
store: ctx.store,
|
|
94
|
+
autoSleep,
|
|
95
|
+
};
|
|
96
|
+
if (project !== undefined)
|
|
97
|
+
mcpCtx.project = project;
|
|
98
|
+
return mcpCtx;
|
|
99
|
+
}
|
|
47
100
|
export async function handleMcpPost(req, res, opts) {
|
|
48
101
|
// Build the same Context the /v1/* routes use so MCP tool calls inherit
|
|
49
102
|
// the server's bound hippoRoot and the auth-resolved tenantId / actor.
|
|
@@ -51,6 +104,7 @@ export async function handleMcpPost(req, res, opts) {
|
|
|
51
104
|
// pull tenant from HIPPO_TENANT, dropping a valid Bearer for tenant B
|
|
52
105
|
// back to whatever the env says.
|
|
53
106
|
const ctx = await buildContextWithAuth(req, opts);
|
|
107
|
+
const project = callerProjectFromHeaders(req, ctx.hippoRoot);
|
|
54
108
|
const raw = await readBody(req);
|
|
55
109
|
let mcpReq;
|
|
56
110
|
try {
|
|
@@ -68,18 +122,7 @@ export async function handleMcpPost(req, res, opts) {
|
|
|
68
122
|
const rpcReq = mcpReq;
|
|
69
123
|
let mcpRes;
|
|
70
124
|
try {
|
|
71
|
-
mcpRes = await handleMcpRequest(rpcReq,
|
|
72
|
-
hippoRoot: ctx.hippoRoot,
|
|
73
|
-
tenantId: ctx.tenantId,
|
|
74
|
-
// McpContext.actor stays string; extract subject at the boundary.
|
|
75
|
-
actor: ctx.actor.subject,
|
|
76
|
-
// The caller's real role: MCP tools must not run a member key as admin.
|
|
77
|
-
role: ctx.actor.role,
|
|
78
|
-
scopes: ctx.actor.scopes,
|
|
79
|
-
viaAuthResolver: ctx.actor.viaAuthResolver,
|
|
80
|
-
hostAdmin: ctx.actor.hostAdmin,
|
|
81
|
-
clientKey: buildMcpClientKey(req),
|
|
82
|
-
});
|
|
125
|
+
mcpRes = await handleMcpRequest(rpcReq, mcpContextFor(ctx, buildMcpClientKey(req), opts.autoSleep, project));
|
|
83
126
|
}
|
|
84
127
|
catch (err) {
|
|
85
128
|
mcpRes = mcpErrorResponse(rpcReq.id, err, requestIds.get(req));
|
|
@@ -107,7 +150,7 @@ function acquireStreamSlot(req, res, slots) {
|
|
|
107
150
|
const key = streamSlotKey(req);
|
|
108
151
|
const open = slots.get(key) ?? 0;
|
|
109
152
|
if (open >= max)
|
|
110
|
-
throw new HttpError(429, `too many open streams for this client (limit ${max}); close one first
|
|
153
|
+
throw new HttpError(429, `too many open streams for this client (limit ${max}); close one first`, 60);
|
|
111
154
|
slots.set(key, open + 1);
|
|
112
155
|
res.once('close', () => {
|
|
113
156
|
const left = (slots.get(key) ?? 1) - 1;
|
package/dist/server/request.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
/** The caller's `X-Request-Id` when it is a short plain token, else a fresh UUID. */
|
|
3
3
|
export declare function resolveRequestId(header: string | string[] | undefined): string;
|
|
4
|
-
/** One line per failed request; 4xx is the caller's mistake
|
|
4
|
+
/** One line per failed request; 4xx is the caller's mistake, a busy 503 is back-pressure and a 501 is a route not on this store, so none logs as an error. */
|
|
5
5
|
export declare function logRequestFailure<E>(req: IncomingMessage, err: E, requestId: string, status: number): void;
|
|
6
6
|
/** The status and client message for a failed request; a held write lock is a retryable 503, never a 500. */
|
|
7
7
|
export declare function replyFor<E>(err: E): {
|
package/dist/server/request.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { isStoreBusy, STORE_BUSY_MESSAGE } from '../db.js';
|
|
3
3
|
import { errorFields, log } from '../log.js';
|
|
4
4
|
import { HttpError, mapApiError, sendJson } from '../http-util.js';
|
|
5
5
|
// The caller's id lands in a response header and in logs, so only a short plain token is echoed back.
|
|
@@ -9,20 +9,20 @@ export function resolveRequestId(header) {
|
|
|
9
9
|
const value = Array.isArray(header) ? undefined : header;
|
|
10
10
|
return value && REQUEST_ID_RE.test(value) ? value : randomUUID();
|
|
11
11
|
}
|
|
12
|
-
/** One line per failed request; 4xx is the caller's mistake
|
|
12
|
+
/** One line per failed request; 4xx is the caller's mistake, a busy 503 is back-pressure and a 501 is a route not on this store, so none logs as an error. */
|
|
13
13
|
export function logRequestFailure(req, err, requestId, status) {
|
|
14
14
|
const message = err instanceof Error ? err.message : String(err);
|
|
15
15
|
const line = `${req.method ?? 'GET'} ${(req.url ?? '/').split('?')[0]} failed: ${message}`;
|
|
16
|
-
if (
|
|
16
|
+
if (isStoreBusy(err))
|
|
17
17
|
log.warn(line, { requestId, status });
|
|
18
|
-
else if (status >= 500)
|
|
18
|
+
else if (status >= 500 && status !== 501)
|
|
19
19
|
log.error(line, { requestId, status, ...errorFields(err) });
|
|
20
20
|
else
|
|
21
21
|
log.info(line, { requestId, status });
|
|
22
22
|
}
|
|
23
23
|
/** The status and client message for a failed request; a held write lock is a retryable 503, never a 500. */
|
|
24
24
|
export function replyFor(err) {
|
|
25
|
-
return
|
|
25
|
+
return isStoreBusy(err) ? { status: 503, message: STORE_BUSY_MESSAGE } : mapApiError(err);
|
|
26
26
|
}
|
|
27
27
|
export function sendError(res, status, message) {
|
|
28
28
|
sendJson(res, status, { error: message });
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// Admin routes: API keys, quarantine and the audit log.
|
|
2
2
|
import { AUDIT_OPS } from '../../audit.js';
|
|
3
3
|
import { auditList, authCreate, authListRows, authRevoke, quarantineApprove, quarantineList, quarantineReject } from '../../api.js';
|
|
4
|
-
import { HttpError, sendJson } from '../../http-util.js';
|
|
4
|
+
import { HttpError, readBody, sendJson } from '../../http-util.js';
|
|
5
5
|
import { assertCrossTenantAdmin, buildContextWithAuth } from '../auth.js';
|
|
6
6
|
import { pageOf, parseCursor, setNextCursorHeader } from '../cursor.js';
|
|
7
|
-
import { isSetMember,
|
|
7
|
+
import { isSetMember, parseJsonObjectText, parseListLimit, validateIdSegment } from '../validation.js';
|
|
8
8
|
import { isJsonString } from '../../json.js';
|
|
9
9
|
const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
|
|
10
|
+
// POST /v1/auth/keys reads its body before auth, so an unauthenticated caller can make the server buffer at most this many bytes and wait at most this many ms.
|
|
11
|
+
// ServeOpts.mintBodyDeadlineMs overrides the wait for tests only, so a 408 test need not sit through it.
|
|
12
|
+
const MINT_BODY_MAX_BYTES = 4 * 1024;
|
|
13
|
+
const MINT_BODY_DEADLINE_MS = 10_000;
|
|
10
14
|
// Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
|
|
11
15
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
12
16
|
// small enough that a malicious client can't ask for the world.
|
|
@@ -17,8 +21,10 @@ const MAX_AUTH_KEYS_PAGE = 1000;
|
|
|
17
21
|
// body: the HTTP layer hands it to the client; the user-facing
|
|
18
22
|
// "store this somewhere safe" warning belongs in the CLI client, not here.
|
|
19
23
|
export async function handleCreateAuthKey({ req, res, opts }) {
|
|
24
|
+
// Body first, so the resolver's check (and any gate in it) runs right before the mint with no wait between.
|
|
25
|
+
const raw = await readBody(req, { maxBytes: MINT_BODY_MAX_BYTES, deadlineMs: opts.mintBodyDeadlineMs ?? MINT_BODY_DEADLINE_MS });
|
|
20
26
|
const ctx = await buildContextWithAuth(req, opts);
|
|
21
|
-
const body =
|
|
27
|
+
const body = parseJsonObjectText(raw);
|
|
22
28
|
const labelRaw = body['label'];
|
|
23
29
|
if (labelRaw !== undefined && !isJsonString(labelRaw)) {
|
|
24
30
|
throw new HttpError(400, 'label must be a string');
|
|
@@ -47,7 +53,7 @@ export async function handleCreateAuthKey({ req, res, opts }) {
|
|
|
47
53
|
}
|
|
48
54
|
// GET /v1/auth/keys?active=true&limit=&cursor=: list keys visible to ctx.tenantId.
|
|
49
55
|
// `active` defaults to true so the common case (show me usable keys) is
|
|
50
|
-
// a single GET; ?active=false includes revoked rows.
|
|
56
|
+
// a single GET; ?active=false includes revoked and expired rows.
|
|
51
57
|
export async function handleListAuthKeys({ req, res, opts, query }) {
|
|
52
58
|
const activeRaw = query.get('active');
|
|
53
59
|
let active = true;
|
|
@@ -67,8 +73,8 @@ export async function handleListAuthKeys({ req, res, opts, query }) {
|
|
|
67
73
|
sendJson(res, 200, page.items.map((r) => r.key));
|
|
68
74
|
return;
|
|
69
75
|
}
|
|
70
|
-
// DELETE /v1/auth/keys/:keyId — revoke. Missing or cross-tenant keys are 404
|
|
71
|
-
//
|
|
76
|
+
// DELETE /v1/auth/keys/:keyId — revoke. Missing or cross-tenant keys are 404 (no info leak); a member
|
|
77
|
+
// is 403 on any key but its own key or, signed in through the resolver, the keys it minted.
|
|
72
78
|
// 200 with the body rather than 204 so the caller sees revokedAt.
|
|
73
79
|
export async function handleRevokeAuthKey({ req, res, opts }, keyMatch) {
|
|
74
80
|
validateIdSegment(keyMatch.keyId, 'key id');
|
|
@@ -3,8 +3,9 @@ import { archiveRaw, forget, outcome, outcomeForLastRecall, promote, remember, s
|
|
|
3
3
|
import { buildGraphModel } from '../../graph-view.js';
|
|
4
4
|
import { MAX_ENTITY_NAME_LEN } from '../../graph/types.js';
|
|
5
5
|
import { HttpError, sendJson } from '../../http-util.js';
|
|
6
|
+
import { canReadScope } from '../../recall-scope.js';
|
|
6
7
|
import { assertCrossTenantAdmin, buildContextWithAuth, isLoopback } from '../auth.js';
|
|
7
|
-
import { getString, getStringArray, isJsonBoolean, isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
|
|
8
|
+
import { getCallerProject, getString, getStringArray, isJsonBoolean, isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
|
|
8
9
|
import { isJsonString } from '../../json.js';
|
|
9
10
|
const VALID_KINDS = new Set([
|
|
10
11
|
'raw',
|
|
@@ -24,6 +25,10 @@ export async function handleCreateMemory({ req, res, opts }) {
|
|
|
24
25
|
if (kindRaw !== undefined && !isSetMember(VALID_KINDS, kindRaw)) {
|
|
25
26
|
throw new HttpError(400, `invalid kind: ${kindRaw}`);
|
|
26
27
|
}
|
|
28
|
+
const personalRaw = body['personal'];
|
|
29
|
+
if (personalRaw !== undefined && !isJsonBoolean(personalRaw)) {
|
|
30
|
+
throw new HttpError(400, 'personal must be a boolean');
|
|
31
|
+
}
|
|
27
32
|
const result = remember(ctx, {
|
|
28
33
|
content,
|
|
29
34
|
kind: kindRaw,
|
|
@@ -31,6 +36,8 @@ export async function handleCreateMemory({ req, res, opts }) {
|
|
|
31
36
|
owner: getString(body, 'owner'),
|
|
32
37
|
artifactRef: getString(body, 'artifactRef'),
|
|
33
38
|
tags: getStringArray(body, 'tags'),
|
|
39
|
+
project: getCallerProject(body),
|
|
40
|
+
personal: personalRaw === true,
|
|
34
41
|
});
|
|
35
42
|
sendJson(res, 200, result);
|
|
36
43
|
return;
|
|
@@ -48,6 +55,7 @@ export async function handleGetGraph({ req, res, opts, query }) {
|
|
|
48
55
|
const model = buildGraphModel(ctx.hippoRoot, ctx.tenantId, {
|
|
49
56
|
entity: entityRaw ?? undefined,
|
|
50
57
|
limit,
|
|
58
|
+
canRead: (s) => s === null || canReadScope(ctx.actor, s),
|
|
51
59
|
});
|
|
52
60
|
sendJson(res, 200, model);
|
|
53
61
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Recall routes: /v1/memories search, assemble, drill and /v1/context.
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import { resolveProjectIdentity } from '../../project-identity.js';
|
|
3
|
+
import { assertCallerProject, resolveProjectIdentity } from '../../project-identity.js';
|
|
4
|
+
import { isSharedStore } from '../../config.js';
|
|
4
5
|
import { assembleCost, contextCost, drillCost } from '../../context-render.js';
|
|
5
6
|
import { closeHippoDb, openHippoDb } from '../../db.js';
|
|
6
7
|
import { updateStats } from '../../store/index-and-stats.js';
|
|
@@ -272,6 +273,17 @@ export async function handleDrillRecall({ req, res, opts, query }, drillMatch) {
|
|
|
272
273
|
sendJson(res, 200, result);
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
276
|
+
/** A shared store's folder is no caller's project, so the caller names it (`project`, repeated `alias`); with none, getContext refuses. */
|
|
277
|
+
function contextReader(hippoRoot, query) {
|
|
278
|
+
if (!isSharedStore(hippoRoot))
|
|
279
|
+
return resolveProjectIdentity(dirname(resolve(hippoRoot)));
|
|
280
|
+
const name = query.get('project');
|
|
281
|
+
if (name === null)
|
|
282
|
+
return '';
|
|
283
|
+
const aliases = query.getAll('alias');
|
|
284
|
+
assertCallerProject({ name, aliases });
|
|
285
|
+
return { name, legacyName: name, aliases };
|
|
286
|
+
}
|
|
275
287
|
// GET /v1/context — assemble a budget-bounded context bundle. Returns
|
|
276
288
|
// ContextResult JSON (entries + tokens + activeSnapshot + sessionHandoff
|
|
277
289
|
// + recentEvents). No server-side rendering; clients render. Tenant-scoped
|
|
@@ -332,7 +344,7 @@ export async function handleGetContext({ req, res, opts, query }) {
|
|
|
332
344
|
scope,
|
|
333
345
|
includeRecent,
|
|
334
346
|
crossProject,
|
|
335
|
-
currentProject:
|
|
347
|
+
currentProject: contextReader(opts.hippoRoot, query),
|
|
336
348
|
cost: contextCost('markdown', 'observe'), // clients render; the budget prices the block `hippo context` would print
|
|
337
349
|
});
|
|
338
350
|
recordTokens(ctx, 'http_context', { items: result.entries.length, tokens: result.tokens });
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { Context } from '../api.js';
|
|
3
|
+
import type { JsonValue } from '../json.js';
|
|
4
|
+
import type { RateLimiter } from '../rate-limit.js';
|
|
5
|
+
import type { HippoStore } from '../store-port.js';
|
|
2
6
|
export interface ServerHandle {
|
|
3
7
|
port: number;
|
|
4
8
|
url: string;
|
|
@@ -19,6 +23,20 @@ export interface ResolvedBearer {
|
|
|
19
23
|
}
|
|
20
24
|
/** 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. */
|
|
21
25
|
export type AuthResolver = (token: string) => ResolvedBearer | null | Promise<ResolvedBearer | null>;
|
|
26
|
+
/** What an add-on route's handler gets: the caller core authenticated and the parsed JSON body. */
|
|
27
|
+
export interface AddonCall {
|
|
28
|
+
readonly ctx: Context;
|
|
29
|
+
readonly body: Readonly<Record<string, JsonValue>>;
|
|
30
|
+
}
|
|
31
|
+
/** A POST /v1 route an add-on mounts through serve(): core authenticates and parses first, then sends the returned value as 200 JSON. */
|
|
32
|
+
export interface AddonRoute {
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly handler: (call: AddonCall) => Promise<JsonValue>;
|
|
35
|
+
}
|
|
36
|
+
export interface RateLimitSpec {
|
|
37
|
+
ratePerSec: number;
|
|
38
|
+
burst: number;
|
|
39
|
+
}
|
|
22
40
|
export interface ServeOpts {
|
|
23
41
|
hippoRoot: string;
|
|
24
42
|
/** Runs on every request and SSE heartbeat, so keep it cache-backed; API keys never reach it. */
|
|
@@ -31,26 +49,45 @@ export interface ServeOpts {
|
|
|
31
49
|
handleSignals?: boolean;
|
|
32
50
|
/** How long stop() lets in-flight requests finish before closing their sockets; defaults to 5000 ms. */
|
|
33
51
|
shutdownDrainMs?: number;
|
|
52
|
+
/** Defaults to hippo.db under `hippoRoot`. A store of another kind runs only the routes ported to it; its caller closes it. */
|
|
53
|
+
store?: HippoStore;
|
|
54
|
+
autoSleep?: false;
|
|
55
|
+
routes?: readonly AddonRoute[];
|
|
56
|
+
mintBodyDeadlineMs?: number;
|
|
57
|
+
/** Static JSON served to anyone at GET <path>; built once at boot and never authenticated, so it must hold nothing secret. */
|
|
58
|
+
publicJson?: Readonly<Record<string, JsonValue>>;
|
|
59
|
+
/** Request limits: perCaller after auth; perAddress replaces HIPPO_V1_RPS when set, 'off' disables it; failedAuthPerAddress guards scrypt. */
|
|
60
|
+
rateLimits?: {
|
|
61
|
+
perCaller?: RateLimitSpec;
|
|
62
|
+
perAddress?: RateLimitSpec | 'off';
|
|
63
|
+
failedAuthPerAddress?: RateLimitSpec;
|
|
64
|
+
};
|
|
34
65
|
}
|
|
66
|
+
export type ResolvedServeOpts = ServeOpts & {
|
|
67
|
+
store: HippoStore;
|
|
68
|
+
publicJsonBodies: ReadonlyMap<string, string>;
|
|
69
|
+
callerLimiter?: RateLimiter;
|
|
70
|
+
failedAuthLimiter: RateLimiter;
|
|
71
|
+
};
|
|
35
72
|
/** Per-request values the /v1 route handlers read. */
|
|
36
73
|
export interface RouteRequest {
|
|
37
74
|
req: IncomingMessage;
|
|
38
75
|
res: ServerResponse;
|
|
39
|
-
opts:
|
|
76
|
+
opts: ResolvedServeOpts;
|
|
40
77
|
query: URLSearchParams;
|
|
41
78
|
}
|
|
42
|
-
/** One /v1 route: an exact path, a matchPath pattern, or a regex, each paired with the handler for one method. */
|
|
79
|
+
/** One /v1 route: an exact path, a matchPath pattern, or a regex, each paired with the handler for one method. `storeReady` routes run under any store. */
|
|
43
80
|
export type Route = {
|
|
44
81
|
method: string;
|
|
82
|
+
storeReady?: true;
|
|
83
|
+
} & ({
|
|
45
84
|
path: string;
|
|
46
85
|
handler: (r: RouteRequest) => Promise<void>;
|
|
47
86
|
} | {
|
|
48
|
-
method: string;
|
|
49
87
|
pattern: string;
|
|
50
88
|
handler: (r: RouteRequest, params: Record<string, string>) => Promise<void>;
|
|
51
89
|
} | {
|
|
52
|
-
method: string;
|
|
53
90
|
regex: RegExp;
|
|
54
91
|
handler: (r: RouteRequest, match: RegExpMatchArray) => Promise<void>;
|
|
55
|
-
};
|
|
92
|
+
});
|
|
56
93
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
|
-
import type { Context } from '../api.js';
|
|
2
|
+
import type { Context, RememberOpts } from '../api.js';
|
|
3
3
|
import { type JsonValue } from '../json.js';
|
|
4
4
|
export declare function isJsonNumber(value: JsonValue | undefined): value is number;
|
|
5
5
|
export declare function isJsonBoolean(value: JsonValue | undefined): value is boolean;
|
|
6
6
|
export declare function isSetMember<T extends string>(set: ReadonlySet<T>, value: string): value is T;
|
|
7
7
|
export declare function parseListLimit(limitRaw: string | null, defaultLimit?: number, maxLimit?: number): number;
|
|
8
|
-
/** `_authed` is proof the caller passed auth
|
|
8
|
+
/** `_authed` is proof the caller passed auth, so a full-size body is read only for an authenticated caller; the key mint reads first under its own small cap. */
|
|
9
9
|
export declare function parseJsonBody(req: IncomingMessage, _authed: Context): Promise<Record<string, JsonValue>>;
|
|
10
|
+
export declare function parseJsonObjectText(raw: string): Record<string, JsonValue>;
|
|
10
11
|
export declare function getString(obj: Record<string, JsonValue>, key: string): string | undefined;
|
|
11
12
|
export declare function getStringArray(obj: Record<string, JsonValue>, key: string): string[] | undefined;
|
|
13
|
+
/** Keeps only `name` and `aliases`: a client's `legacy_name` is a folder name, which must never become a row's origin. */
|
|
14
|
+
export declare function getCallerProject(body: Record<string, JsonValue>): RememberOpts['project'];
|
|
12
15
|
export declare function validateIdSegment(id: string, fieldName: string): void;
|
|
13
16
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -30,9 +30,11 @@ export function parseListLimit(limitRaw, defaultLimit = 100, maxLimit = 1000) {
|
|
|
30
30
|
}
|
|
31
31
|
return limit;
|
|
32
32
|
}
|
|
33
|
-
/** `_authed` is proof the caller passed auth
|
|
33
|
+
/** `_authed` is proof the caller passed auth, so a full-size body is read only for an authenticated caller; the key mint reads first under its own small cap. */
|
|
34
34
|
export async function parseJsonBody(req, _authed) {
|
|
35
|
-
|
|
35
|
+
return parseJsonObjectText(await readBody(req));
|
|
36
|
+
}
|
|
37
|
+
export function parseJsonObjectText(raw) {
|
|
36
38
|
if (raw.length === 0)
|
|
37
39
|
return {};
|
|
38
40
|
try {
|
|
@@ -60,6 +62,21 @@ export function getStringArray(obj, key) {
|
|
|
60
62
|
return undefined;
|
|
61
63
|
return v;
|
|
62
64
|
}
|
|
65
|
+
/** Keeps only `name` and `aliases`: a client's `legacy_name` is a folder name, which must never become a row's origin. */
|
|
66
|
+
export function getCallerProject(body) {
|
|
67
|
+
const v = body.project;
|
|
68
|
+
if (v === undefined || v === null)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!isJsonObjectRecord(v) || !isJsonString(v.name)) {
|
|
71
|
+
throw new HttpError(400, 'project must be an object with a "name" string');
|
|
72
|
+
}
|
|
73
|
+
if (v.aliases === undefined)
|
|
74
|
+
return { name: v.name };
|
|
75
|
+
if (!Array.isArray(v.aliases) || !v.aliases.every(isJsonString)) {
|
|
76
|
+
throw new HttpError(400, 'project.aliases must be an array of strings');
|
|
77
|
+
}
|
|
78
|
+
return { name: v.name, aliases: v.aliases };
|
|
79
|
+
}
|
|
63
80
|
/**
|
|
64
81
|
* Charset + length validation for `:id` route captures. Routes call
|
|
65
82
|
* this immediately after `matchPath` to reject empty / overlong / illegal
|
package/dist/server.d.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import { type Actor, authRevoke, type Context } from './api.js';
|
|
1
|
+
import { type Actor, authCreateSelf, type AuthCreateSelfOpts, type AuthCreateSelfResult, authRevoke, type Context } from './api.js';
|
|
2
2
|
import { ForbiddenError } from './api-errors.js';
|
|
3
3
|
import type { ServeOpts, ServerHandle } from './server/types.js';
|
|
4
|
-
export { authRevoke, ForbiddenError, type Context, type Actor };
|
|
4
|
+
export { authCreateSelf, authRevoke, ForbiddenError, type AuthCreateSelfOpts, type AuthCreateSelfResult, type Context, type Actor };
|
|
5
5
|
export { isCrossSite, LOOPBACK_HOST_HEADER } from './http-util.js';
|
|
6
6
|
export { __resetSessionRecallHistoryHttp } from './server/routes/recall.js';
|
|
7
7
|
export { clientIpForRateLimit } from './server/client-ip.js';
|
|
8
8
|
export { isLoopback, isReservedActor } from './server/auth.js';
|
|
9
|
-
export type { AuthResolver, ResolvedBearer, ServeOpts, ServerHandle } from './server/types.js';
|
|
9
|
+
export type { AddonCall, AddonRoute, AuthResolver, RateLimitSpec, ResolvedBearer, ServeOpts, ServerHandle } from './server/types.js';
|
|
10
|
+
export { HttpError } from './http-util.js';
|
|
11
|
+
export { promptHookContext, type CallerProject } from './prompt-hook.js';
|
|
12
|
+
export type { JsonValue } from './json.js';
|
|
13
|
+
export { captureSessionTexts, type SessionCaptureRequest, type SessionCaptureResult } from './capture/session-texts.js';
|
|
14
|
+
export { sqliteStore, type HippoStore } from './store-port.js';
|
|
15
|
+
export type { ApiKeyRecord } from './auth.js';
|
|
16
|
+
export { StoreBusyError } from './db.js';
|
|
17
|
+
export { authCreate, type AuthCreateOpts, type AuthCreateResult } from './api.js';
|
|
18
|
+
export { isSharedStore } from './config.js';
|
|
19
|
+
export { ownerOrSubject } from './api.js';
|
|
20
|
+
export { bindSessionOwner } from './session-owners.js';
|
|
21
|
+
export { preCompactForCaller, type CallerHookOutput, type CallerPreCompactRequest } from './capture/pre-compact-caller.js';
|
|
22
|
+
export { compactResumeForCaller, type CallerCompactResumeRequest } from './capture/compact-resume-caller.js';
|
|
23
|
+
export { saveCompactionItemsForCaller, type CallerItemsRequest, type CallerItemsResult } from './capture/compaction-items-caller.js';
|
|
24
|
+
export { captureFailureForCaller, type CallerFailureRequest, type CallerFailureResult } from './capture/failure-caller.js';
|
|
25
|
+
export { sessionEndHandoffForCaller, type CallerEvidence, type CallerSessionEndRequest, type CallerSessionEndResult } from './capture/session-end-caller.js';
|
|
26
|
+
export type { WorkingState } from './capture/working-state.js';
|
|
10
27
|
/**
|
|
11
28
|
* Boot the HTTP daemon on host:port and write the pidfile under hippoRoot.
|
|
12
29
|
*
|
|
@@ -15,7 +32,7 @@ export type { AuthResolver, ResolvedBearer, ServeOpts, ServerHandle } from './se
|
|
|
15
32
|
* requireAuth) has shipped and every route checks it except GET /health
|
|
16
33
|
* (public by design for platform health checks) and the two connector
|
|
17
34
|
* webhooks in PUBLIC_ROUTES, which are HMAC-gated by their own signing
|
|
18
|
-
* secrets and 404 when those secrets are unset. But the loopback
|
|
35
|
+
* secrets and 404 when those secrets are unset, and any publicJson GET path. But the loopback
|
|
19
36
|
* no-auth fallback inside buildContextWithAuth still admits unauthenticated
|
|
20
37
|
* requests from a loopback remote address (unless they carry Forwarded,
|
|
21
38
|
* X-Forwarded-For/-Host/-Proto, X-Real-IP, Cf-Connecting-Ip or True-Client-Ip, which mark a same-host proxy and get
|