hippo-memory 1.67.0 → 1.69.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 +1 -1
- package/dist/agent-memories/copilot.d.ts +3 -1
- package/dist/agent-memories/copilot.js +2 -2
- package/dist/api/auth.d.ts +14 -18
- package/dist/api/auth.js +93 -60
- package/dist/api/context-select.d.ts +10 -0
- package/dist/api/context-select.js +16 -18
- package/dist/api/context.js +101 -60
- package/dist/api/forget.d.ts +2 -2
- package/dist/api/forget.js +11 -0
- package/dist/api/outcome.d.ts +4 -4
- package/dist/api/outcome.js +29 -6
- package/dist/api/promote.d.ts +3 -3
- package/dist/api/promote.js +45 -90
- package/dist/api/remember.d.ts +3 -2
- package/dist/api/remember.js +38 -11
- package/dist/api/types.d.ts +9 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +14 -6
- package/dist/capture/compact.js +21 -2
- package/dist/capture/copilot-transcript.d.ts +3 -0
- package/dist/capture/copilot-transcript.js +8 -1
- package/dist/capture/session-worker.d.ts +13 -0
- package/dist/capture/session-worker.js +194 -0
- package/dist/capture-contract.d.ts +3 -1
- package/dist/capture-contract.js +36 -0
- package/dist/cli/session-hooks.js +90 -22
- package/dist/cli/setup.js +70 -24
- package/dist/cli/usage.js +4 -1
- package/dist/cli.js +1 -1
- package/dist/connectors/github/ingest.js +2 -1
- package/dist/connectors/slack/deletion.js +2 -1
- package/dist/connectors/slack/ingest.js +2 -1
- package/dist/embeddings.d.ts +10 -10
- package/dist/embeddings.js +103 -18
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -0
- package/dist/hooks/copilot.d.ts +50 -8
- package/dist/hooks/copilot.js +116 -30
- package/dist/hooks/json-hooks.d.ts +2 -0
- package/dist/hooks/json-hooks.js +11 -2
- package/dist/hooks/shared.d.ts +7 -1
- package/dist/hooks/shared.js +18 -3
- package/dist/mcp/memory-tools.d.ts +2 -2
- package/dist/mcp/memory-tools.js +9 -8
- package/dist/mcp/request.js +3 -3
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +7 -0
- package/dist/prompt-recall.d.ts +5 -0
- package/dist/prompt-recall.js +13 -0
- package/dist/raw-archive.d.ts +2 -2
- package/dist/raw-archive.js +5 -3
- package/dist/recall-scope.d.ts +2 -5
- package/dist/recall-scope.js +5 -6
- package/dist/server/routes/admin.js +2 -2
- package/dist/server/routes/memories.js +9 -6
- package/dist/server-detect.js +3 -2
- package/dist/server.d.ts +20 -4
- package/dist/server.js +23 -11
- package/dist/store/audit-event.d.ts +1 -1
- package/dist/store/audit-event.js +1 -1
- package/dist/store/entry-writes-group.d.ts +9 -0
- package/dist/store/entry-writes-group.js +157 -0
- package/dist/store/handoffs.d.ts +5 -2
- package/dist/store/handoffs.js +35 -5
- package/dist/store/key-writes.d.ts +9 -0
- package/dist/store/key-writes.js +30 -0
- package/dist/store/open.d.ts +2 -0
- package/dist/store/open.js +10 -0
- package/dist/store/search-rows.js +3 -9
- package/dist/store/sessions.d.ts +5 -0
- package/dist/store/sessions.js +4 -1
- package/dist/store/vector-writes.d.ts +6 -0
- package/dist/store/vector-writes.js +59 -0
- package/dist/store-port.d.ts +143 -1
- package/dist/store-port.js +48 -12
- 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/README.md
CHANGED
|
@@ -844,7 +844,7 @@ For Codex, it adds two hooks to `$CODEX_HOME/hooks.json` (else `~/.codex/hooks.j
|
|
|
844
844
|
|
|
845
845
|
**Codex runs a new or changed hook only after you trust it, so open `/hooks` in Codex once and trust both;** `hippo doctor` reminds you. The per-prompt hook was checked against a real Codex request; the compaction hook follows Codex's documented `compact` start source and has not been watched end to end in Codex. Each hook also carries a `commandWindows` form (`hippo.cmd ...`), because Codex runs hooks through PowerShell on Windows, where the execution policy can block npm's `hippo.ps1`. hippo only ever appends these two entries and never rewrites one, since Codex treats a changed command as a new hook to trust. To remove: `hippo hook uninstall codex`, which takes out only hippo's exact commands and leaves every other hook, including one of yours that runs hippo.
|
|
846
846
|
|
|
847
|
-
For GitHub Copilot,
|
|
847
|
+
For GitHub Copilot, when `$COPILOT_HOME` (else `~/.copilot`) or a VS Code User folder exists, `hippo setup` writes hippo's own `hooks/hippo.json`. When the Copilot folder was there before setup, it also adds a `hippo` server to `mcp-config.json` and a block to `copilot-instructions.md`. The block asks the agent to call `hippo_recall` and `hippo_remember`. The Copilot CLI, including its sessions inside VS Code, uses the session-start, tool-failure, pre-compact and session-end hooks, the server and the block. VS Code's agent mode (the Local agent, VS Code 1.109.3 or later) reads the same hooks file. It gets session start, the pre-compact snapshot and a capture after every reply. Setup also adds hippo to VS Code's own `mcp.json` and writes `prompts/hippo.instructions.md`, so its recall tools and instructions work too. The hooks, the VS Code files and how to remove them: [integrations/copilot.md](integrations/copilot.md).
|
|
848
848
|
|
|
849
849
|
### What the hook adds (Claude Code example)
|
|
850
850
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type { Adapter } from './types.js';
|
|
1
|
+
import type { Adapter, AdapterContext } from './types.js';
|
|
2
|
+
/** VS Code's user-data resolution: portable install first, then the app-data override, then the platform default. */
|
|
3
|
+
export declare function vscodeDataFolders(ctx: Pick<AdapterContext, 'env' | 'home' | 'platform'>): string[];
|
|
2
4
|
export declare const copilotAdapter: Adapter;
|
|
3
5
|
//# sourceMappingURL=copilot.d.ts.map
|
|
@@ -7,7 +7,7 @@ import { gitLayout } from './git.js';
|
|
|
7
7
|
const PRODUCTS = ['Code', 'Code - Insiders'];
|
|
8
8
|
const MEMORY_TOOL = ['github.copilot-chat', 'memory-tool', 'memories'];
|
|
9
9
|
/** VS Code's user-data resolution: portable install first, then the app-data override, then the platform default. */
|
|
10
|
-
function
|
|
10
|
+
export function vscodeDataFolders(ctx) {
|
|
11
11
|
const { env, home, platform } = ctx;
|
|
12
12
|
if (env.VSCODE_PORTABLE)
|
|
13
13
|
return [path.join(env.VSCODE_PORTABLE, 'user-data')];
|
|
@@ -103,7 +103,7 @@ function projectContainers(data, ctx, matches, warnings) {
|
|
|
103
103
|
export const copilotAdapter = {
|
|
104
104
|
tool: 'copilot',
|
|
105
105
|
list(ctx, scope) {
|
|
106
|
-
const data =
|
|
106
|
+
const data = vscodeDataFolders(ctx);
|
|
107
107
|
const warnings = [];
|
|
108
108
|
const found = [];
|
|
109
109
|
if (scope === 'user') {
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type ApiKeyListItem, type ApiKeyListRow } from '../auth.js';
|
|
2
2
|
import type { KeysetPosition } from '../keyset.js';
|
|
3
|
-
import {
|
|
4
|
-
import type { Context } from './types.js';
|
|
3
|
+
import type { Context, StoreReply } from './types.js';
|
|
5
4
|
export interface AuthCreateOpts {
|
|
6
5
|
label?: string;
|
|
7
6
|
/**
|
|
@@ -30,7 +29,7 @@ export interface AuthCreateResult {
|
|
|
30
29
|
* Only an admin actor can mint (ForbiddenError otherwise), and a key never
|
|
31
30
|
* outranks its minter: a resolver admin is tenant-only, so it mints members.
|
|
32
31
|
*/
|
|
33
|
-
export declare function authCreate(ctx:
|
|
32
|
+
export declare function authCreate<C extends Context>(ctx: C, opts: AuthCreateOpts): StoreReply<C, AuthCreateResult>;
|
|
34
33
|
export interface AuthCreateSelfOpts {
|
|
35
34
|
label?: string;
|
|
36
35
|
/** Days until the new key expires. */
|
|
@@ -42,8 +41,9 @@ export interface AuthCreateSelfResult extends AuthCreateResult {
|
|
|
42
41
|
/** ISO time the key stops working. */
|
|
43
42
|
expiresAt: string;
|
|
44
43
|
}
|
|
45
|
-
/** Mint a member key for the caller an auth resolver vouched for, whatever its role; the binary floor, the cap's revokes, the mint and its audit rows commit or fail together.
|
|
46
|
-
|
|
44
|
+
/** Mint a member key for the caller an auth resolver vouched for, whatever its role; the binary floor, the cap's revokes, the mint and its audit rows commit or fail together.
|
|
45
|
+
* With `ctx.store`, its keyWrites group runs that transaction; hippo.db is opened only when there is no store. */
|
|
46
|
+
export declare function authCreateSelf<C extends Context>(ctx: C, opts: AuthCreateSelfOpts): StoreReply<C, AuthCreateSelfResult>;
|
|
47
47
|
/**
|
|
48
48
|
* List API keys visible to the calling tenant.
|
|
49
49
|
*
|
|
@@ -52,27 +52,22 @@ export declare function authCreateSelf(ctx: Context, opts: AuthCreateSelfOpts):
|
|
|
52
52
|
* scoped because future multi-tenant deployments will share a hippoRoot, and
|
|
53
53
|
* tenant A must not see tenant B's keys. Read-only, so no audit emit.
|
|
54
54
|
*/
|
|
55
|
-
export declare function authList(ctx:
|
|
55
|
+
export declare function authList<C extends Context>(ctx: C, opts: {
|
|
56
56
|
active: boolean;
|
|
57
|
-
}): ApiKeyListItem[]
|
|
58
|
-
|
|
59
|
-
export declare function authListRows(ctx: Context, opts: {
|
|
57
|
+
}): StoreReply<C, ApiKeyListItem[]>;
|
|
58
|
+
type KeyListOpts = {
|
|
60
59
|
active: boolean;
|
|
61
60
|
limit?: number;
|
|
62
61
|
after?: KeysetPosition;
|
|
63
|
-
}
|
|
62
|
+
};
|
|
63
|
+
/** One page of the caller's tenant's keys, newest first, with the row ids a next-page cursor is built from.
|
|
64
|
+
* With `ctx.store`, its keyWrites group reads them; hippo.db is opened only when there is no store. */
|
|
65
|
+
export declare function authListRows<C extends Context>(ctx: C, opts: KeyListOpts): StoreReply<C, ApiKeyListRow[]>;
|
|
64
66
|
export interface AuthRevokeResult {
|
|
65
67
|
ok: true;
|
|
66
68
|
revokedAt: string;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
* hippoRoot keeps the second test off TypeScript's weak-type rule, which would fail a ctx that has no store key at all. */
|
|
70
|
-
export type AuthRevokeReply<C extends Context> = C extends {
|
|
71
|
-
readonly store: HippoStore;
|
|
72
|
-
} ? Promise<AuthRevokeResult> : C extends {
|
|
73
|
-
readonly hippoRoot: string;
|
|
74
|
-
readonly store?: undefined;
|
|
75
|
-
} ? AuthRevokeResult : AuthRevokeResult | Promise<AuthRevokeResult>;
|
|
70
|
+
export type AuthRevokeReply<C extends Context> = StoreReply<C, AuthRevokeResult>;
|
|
76
71
|
/** Revoke a key in the caller's tenant: a member API key may revoke only itself, a resolver member only the keys it minted.
|
|
77
72
|
* With `ctx.store`, its keyAudit group revokes and writes the auth_revoke row; hippo.db is opened only when there is no store. */
|
|
78
73
|
export declare function authRevoke<C extends Context>(ctx: C, keyId: string): AuthRevokeReply<C>;
|
|
@@ -89,4 +84,5 @@ export interface AuthGrantResult {
|
|
|
89
84
|
export declare function authGrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
90
85
|
/** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
|
|
91
86
|
export declare function authUngrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
87
|
+
export {};
|
|
92
88
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/api/auth.js
CHANGED
|
@@ -2,31 +2,17 @@
|
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
3
3
|
import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from '../api-errors.js';
|
|
4
4
|
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
5
|
-
import {
|
|
5
|
+
import { forgetVerifiedKey, insertApiKey, listApiKeyRows, mintApiKey, revokeApiKey, grantScope, ungrantScope, } from '../auth.js';
|
|
6
6
|
import { DAY_MS } from '../dashboard-snapshot.js';
|
|
7
7
|
import { isRestrictedScope } from '../recall-scope.js';
|
|
8
8
|
import { requireGroup } from '../store-port.js';
|
|
9
|
+
import { createAuditOf, createKeyAt, createSelfKeyAt } from '../store/key-writes.js';
|
|
9
10
|
import { selectApiKeyOwner } from '../store/tenant-lookup.js';
|
|
10
11
|
const API_KEY_SUBJECT = 'api_key:';
|
|
11
12
|
/** The key id an API-key actor's subject names, or null for any other actor. */
|
|
12
13
|
function keyIdOfSubject(subject) {
|
|
13
14
|
return subject.startsWith(API_KEY_SUBJECT) ? subject.slice(API_KEY_SUBJECT.length) : null;
|
|
14
15
|
}
|
|
15
|
-
function inTransaction(db, fn) {
|
|
16
|
-
db.exec('BEGIN IMMEDIATE');
|
|
17
|
-
try {
|
|
18
|
-
const out = fn();
|
|
19
|
-
db.exec('COMMIT');
|
|
20
|
-
return out;
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
try {
|
|
24
|
-
db.exec('ROLLBACK');
|
|
25
|
-
}
|
|
26
|
-
catch { /* keep the original error */ }
|
|
27
|
-
throw err;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
16
|
/**
|
|
31
17
|
* Mint a new API key. The new key is ALWAYS bound to `ctx.tenantId`. Callers
|
|
32
18
|
* cannot override the tenant via the opts bag — a previous `tenantId` field
|
|
@@ -39,38 +25,56 @@ function inTransaction(db, fn) {
|
|
|
39
25
|
* outranks its minter: a resolver admin is tenant-only, so it mints members.
|
|
40
26
|
*/
|
|
41
27
|
export function authCreate(ctx, opts) {
|
|
28
|
+
// With a store, its keyWrites group stores the key and its auth_create row together, and hippo.db is never opened.
|
|
29
|
+
const reply = ctx.store ? createThroughStore(ctx, ctx.store, opts) : createOnHippoDb(ctx, opts);
|
|
30
|
+
// SAFETY: as in authRevoke, a C typed with a store gets the promise its path returns and a C that hides one gets the union.
|
|
31
|
+
return reply;
|
|
32
|
+
}
|
|
33
|
+
/** A key row in the caller's tenant, made here so only its hash ever reaches a store. */
|
|
34
|
+
function newKey(ctx, fields) {
|
|
35
|
+
const { keyId, plaintext, keyHash } = mintApiKey();
|
|
36
|
+
return { plaintext, key: { ...fields, keyId, keyHash, tenantId: ctx.tenantId, createdAt: new Date().toISOString() } };
|
|
37
|
+
}
|
|
38
|
+
function adminKeyMint(ctx, opts) {
|
|
42
39
|
if (ctx.actor.role !== 'admin') {
|
|
43
40
|
throw new ForbiddenError('Only an admin key can create API keys');
|
|
44
41
|
}
|
|
45
42
|
if (ctx.actor.viaAuthResolver && opts.role === 'admin') {
|
|
46
43
|
throw new ForbiddenError('A key minted through the auth resolver can only be a member key');
|
|
47
44
|
}
|
|
45
|
+
const role = opts.role ?? (ctx.actor.viaAuthResolver ? 'member' : 'admin');
|
|
46
|
+
const label = opts.label ?? null;
|
|
47
|
+
const { plaintext, key } = newKey(ctx, { label, role, ownerSubject: null, expiresAt: null });
|
|
48
|
+
// The plaintext is NEVER logged; metadata carries label + role, keyId is non-secret.
|
|
49
|
+
return { plaintext, mint: { key, actor: ctx.actor.subject, metadata: { label, role } } };
|
|
50
|
+
}
|
|
51
|
+
function createResult({ key }, plaintext) {
|
|
52
|
+
return { keyId: key.keyId, plaintext, tenantId: key.tenantId, role: key.role };
|
|
53
|
+
}
|
|
54
|
+
async function createThroughStore(ctx, store, opts) {
|
|
55
|
+
const keyWrites = requireGroup(store, 'keyWrites');
|
|
56
|
+
const { plaintext, mint } = adminKeyMint(ctx, opts);
|
|
57
|
+
await keyWrites.createApiKey(mint);
|
|
58
|
+
return createResult(mint, plaintext);
|
|
59
|
+
}
|
|
60
|
+
function createOnHippoDb(ctx, opts) {
|
|
61
|
+
const { plaintext, mint } = adminKeyMint(ctx, opts);
|
|
48
62
|
const db = openHippoDb(ctx.hippoRoot);
|
|
49
63
|
try {
|
|
50
|
-
const role = opts.role ?? (ctx.actor.viaAuthResolver ? 'member' : 'admin');
|
|
51
|
-
const mint = () => createApiKey(db, { tenantId: ctx.tenantId, label: opts.label, role });
|
|
52
|
-
// The plaintext is NEVER logged; metadata carries label + role, keyId is non-secret.
|
|
53
|
-
const audit = (keyId) => appendAuditEvent(db, {
|
|
54
|
-
tenantId: ctx.tenantId, actor: ctx.actor.subject, op: 'auth_create', targetId: keyId, metadata: { label: opts.label ?? null, role },
|
|
55
|
-
});
|
|
56
64
|
// A resolver admin's keys are found again only through their audit rows, so the key and its row commit together.
|
|
57
65
|
if (ctx.actor.viaAuthResolver) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
audit(minted.keyId);
|
|
61
|
-
return minted;
|
|
62
|
-
});
|
|
63
|
-
return { keyId: result.keyId, plaintext: result.plaintext, tenantId: ctx.tenantId, role };
|
|
66
|
+
createKeyAt(db, mint);
|
|
67
|
+
return createResult(mint, plaintext);
|
|
64
68
|
}
|
|
65
|
-
|
|
69
|
+
insertApiKey(db, mint.key);
|
|
66
70
|
try {
|
|
67
|
-
|
|
71
|
+
appendAuditEvent(db, createAuditOf(mint));
|
|
68
72
|
}
|
|
69
73
|
catch (error) {
|
|
70
74
|
// Audit must not crash a successful mint.
|
|
71
|
-
reportAuditWriteFailure('auth_create', String(error),
|
|
75
|
+
reportAuditWriteFailure('auth_create', String(error), mint.key.keyId);
|
|
72
76
|
}
|
|
73
|
-
return
|
|
77
|
+
return createResult(mint, plaintext);
|
|
74
78
|
}
|
|
75
79
|
finally {
|
|
76
80
|
closeHippoDb(db);
|
|
@@ -86,34 +90,43 @@ function assertSelfMintOpts({ ttlDays, perSubject }) {
|
|
|
86
90
|
throw new RangeError(`authCreateSelf: perSubject must be a whole number of at least 1, got ${String(perSubject)}`);
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
|
-
/** Mint a member key for the caller an auth resolver vouched for, whatever its role; the binary floor, the cap's revokes, the mint and its audit rows commit or fail together.
|
|
93
|
+
/** Mint a member key for the caller an auth resolver vouched for, whatever its role; the binary floor, the cap's revokes, the mint and its audit rows commit or fail together.
|
|
94
|
+
* With `ctx.store`, its keyWrites group runs that transaction; hippo.db is opened only when there is no store. */
|
|
90
95
|
export function authCreateSelf(ctx, opts) {
|
|
96
|
+
const reply = ctx.store ? createSelfThroughStore(ctx, ctx.store, opts) : createSelfOnHippoDb(ctx, opts);
|
|
97
|
+
// SAFETY: as in authRevoke, a C typed with a store gets the promise its path returns and a C that hides one gets the union.
|
|
98
|
+
return reply;
|
|
99
|
+
}
|
|
100
|
+
function selfKeyMint(ctx, opts) {
|
|
91
101
|
if (!ctx.actor.viaAuthResolver) {
|
|
92
102
|
throw new ForbiddenError('Only a caller signed in through the auth resolver can mint its own key');
|
|
93
103
|
}
|
|
94
104
|
assertSelfMintOpts(opts);
|
|
95
|
-
const {
|
|
96
|
-
const
|
|
97
|
-
const expiresAt = new Date(now + opts.ttlDays * DAY_MS).toISOString();
|
|
105
|
+
const { subject } = ctx.actor;
|
|
106
|
+
const label = opts.label ?? null;
|
|
107
|
+
const expiresAt = new Date(Date.now() + opts.ttlDays * DAY_MS).toISOString();
|
|
108
|
+
// Not destructured: a binding pattern makes TypeScript infer F as KeyFields and lose ownerSubject's string type.
|
|
109
|
+
const made = newKey(ctx, { label, role: 'member', ownerSubject: subject, expiresAt });
|
|
110
|
+
// Same op and actor as an admin mint, so a lookup by audit row finds this key too.
|
|
111
|
+
return { plaintext: made.plaintext, mint: { key: made.key, actor: subject, metadata: { label, role: 'member', self: true, expiresAt }, perSubject: opts.perSubject } };
|
|
112
|
+
}
|
|
113
|
+
function selfResult({ key }, plaintext) {
|
|
114
|
+
return { keyId: key.keyId, plaintext, tenantId: key.tenantId, role: 'member', expiresAt: key.expiresAt };
|
|
115
|
+
}
|
|
116
|
+
/** The store revokes the replaced keys without this process's verified-key cache, so they leave it here once the mint commits. */
|
|
117
|
+
async function createSelfThroughStore(ctx, store, opts) {
|
|
118
|
+
const keyWrites = requireGroup(store, 'keyWrites');
|
|
119
|
+
const { plaintext, mint } = selfKeyMint(ctx, opts);
|
|
120
|
+
for (const keyId of await keyWrites.createSelfApiKey(mint))
|
|
121
|
+
forgetVerifiedKey(keyId);
|
|
122
|
+
return selfResult(mint, plaintext);
|
|
123
|
+
}
|
|
124
|
+
function createSelfOnHippoDb(ctx, opts) {
|
|
125
|
+
const { plaintext, mint } = selfKeyMint(ctx, opts);
|
|
98
126
|
const db = openHippoDb(ctx.hippoRoot);
|
|
99
127
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const live = listLiveOwnedKeyIds(db, tenantId, subject, now);
|
|
103
|
-
const replaced = live.slice(0, Math.max(0, live.length - opts.perSubject + 1));
|
|
104
|
-
const minted = createApiKey(db, { tenantId, label: opts.label, role: 'member', ownerSubject: subject, expiresAt });
|
|
105
|
-
for (const keyId of replaced) {
|
|
106
|
-
revokeApiKey(db, keyId);
|
|
107
|
-
appendAuditEvent(db, { tenantId, actor: subject, op: 'auth_revoke', targetId: keyId, metadata: { replacedBy: minted.keyId } });
|
|
108
|
-
}
|
|
109
|
-
// Same op and actor as an admin mint, so a lookup by audit row finds this key too.
|
|
110
|
-
appendAuditEvent(db, {
|
|
111
|
-
tenantId, actor: subject, op: 'auth_create', targetId: minted.keyId,
|
|
112
|
-
metadata: { label: opts.label ?? null, role: 'member', self: true, expiresAt },
|
|
113
|
-
});
|
|
114
|
-
return minted;
|
|
115
|
-
});
|
|
116
|
-
return { keyId: result.keyId, plaintext: result.plaintext, tenantId, role: 'member', expiresAt };
|
|
128
|
+
createSelfKeyAt(db, mint);
|
|
129
|
+
return selfResult(mint, plaintext);
|
|
117
130
|
}
|
|
118
131
|
finally {
|
|
119
132
|
closeHippoDb(db);
|
|
@@ -128,30 +141,50 @@ export function authCreateSelf(ctx, opts) {
|
|
|
128
141
|
* tenant A must not see tenant B's keys. Read-only, so no audit emit.
|
|
129
142
|
*/
|
|
130
143
|
export function authList(ctx, opts) {
|
|
131
|
-
|
|
144
|
+
const keysOf = (rows) => rows.map((r) => r.key);
|
|
145
|
+
const rows = listRows(ctx, opts);
|
|
146
|
+
// SAFETY: the promise comes back exactly when ctx has a store, as StoreReply says.
|
|
147
|
+
return (rows instanceof Promise ? rows.then(keysOf) : keysOf(rows));
|
|
132
148
|
}
|
|
133
|
-
/** One page of the caller's tenant's keys, newest first, with the row ids a next-page cursor is built from.
|
|
149
|
+
/** One page of the caller's tenant's keys, newest first, with the row ids a next-page cursor is built from.
|
|
150
|
+
* With `ctx.store`, its keyWrites group reads them; hippo.db is opened only when there is no store. */
|
|
134
151
|
export function authListRows(ctx, opts) {
|
|
152
|
+
// SAFETY: the promise comes back exactly when ctx has a store, as StoreReply says.
|
|
153
|
+
return listRows(ctx, opts);
|
|
154
|
+
}
|
|
155
|
+
function listRows(ctx, opts) {
|
|
156
|
+
return ctx.store ? listThroughStore(ctx, ctx.store, opts) : listOnHippoDb(ctx, opts);
|
|
157
|
+
}
|
|
158
|
+
async function listThroughStore(ctx, store, opts) {
|
|
159
|
+
const keyWrites = requireGroup(store, 'keyWrites');
|
|
160
|
+
const keyId = memberKeyIdOf(ctx.actor);
|
|
161
|
+
const filter = memberListFilter(ctx.actor, keyId === null ? undefined : (await store.findApiKey(keyId))?.ownerSubject);
|
|
162
|
+
return filter === null ? [] : keyWrites.listApiKeys({ ...opts, tenantId: ctx.tenantId, ...filter });
|
|
163
|
+
}
|
|
164
|
+
function listOnHippoDb(ctx, opts) {
|
|
135
165
|
const db = openHippoDb(ctx.hippoRoot);
|
|
136
166
|
try {
|
|
137
|
-
const
|
|
167
|
+
const keyId = memberKeyIdOf(ctx.actor);
|
|
168
|
+
const filter = memberListFilter(ctx.actor, keyId === null ? undefined : selectApiKeyOwner(db, keyId)?.ownerSubject);
|
|
138
169
|
return filter === null ? [] : listApiKeyRows(db, { ...opts, tenantId: ctx.tenantId, ...filter });
|
|
139
170
|
}
|
|
140
171
|
finally {
|
|
141
172
|
closeHippoDb(db);
|
|
142
173
|
}
|
|
143
174
|
}
|
|
175
|
+
/** The key id of a member API key caller, whose owner decides what it lists; null for any other caller. */
|
|
176
|
+
function memberKeyIdOf(actor) {
|
|
177
|
+
return actor.role === 'admin' || actor.viaAuthResolver ? null : keyIdOfSubject(actor.subject);
|
|
178
|
+
}
|
|
144
179
|
/** A member sees only the keys its person minted, or just its own key when that has no owner; an admin sees the whole tenant; null means no keys. */
|
|
145
|
-
function memberListFilter(
|
|
146
|
-
const { actor } = ctx;
|
|
180
|
+
function memberListFilter(actor, owner) {
|
|
147
181
|
if (actor.role === 'admin')
|
|
148
182
|
return {};
|
|
149
183
|
if (actor.viaAuthResolver)
|
|
150
184
|
return { ownerSubject: actor.subject };
|
|
151
|
-
const keyId =
|
|
185
|
+
const keyId = memberKeyIdOf(actor);
|
|
152
186
|
if (keyId === null)
|
|
153
187
|
return null;
|
|
154
|
-
const owner = selectApiKeyOwner(db, keyId)?.ownerSubject;
|
|
155
188
|
return owner ? { ownerSubject: owner } : { keyId };
|
|
156
189
|
}
|
|
157
190
|
/** Throws unless `ctx` may revoke `keyId`; a member gets one answer for a missing, foreign or unowned key, so it cannot probe key ids. */
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { AmbientLoadResult } from '../store/candidates.js';
|
|
2
2
|
import { type MemoryEntry } from '../memory.js';
|
|
3
|
+
import type { ContextReads, HippoStore } from '../store-port.js';
|
|
3
4
|
import { type HippoConfig } from '../config.js';
|
|
4
5
|
import type { ProjectRef } from '../project-identity.js';
|
|
5
6
|
import type { DeliveryObserver } from '../delivery-recorder.js';
|
|
6
7
|
import type { ContextCost, ContextOpts, ContextResultEntry } from './context-types.js';
|
|
7
8
|
import type { Context } from './types.js';
|
|
9
|
+
/** One store getContext reads rows from: its base search and its context reads. */
|
|
10
|
+
export interface ContextSource {
|
|
11
|
+
readonly store: HippoStore;
|
|
12
|
+
readonly reads: ContextReads;
|
|
13
|
+
}
|
|
8
14
|
/** What getContext resolved from opts and config before it read a row. */
|
|
9
15
|
export interface ContextPlan {
|
|
16
|
+
/** The served store, else hippo.db under the root. */
|
|
17
|
+
local: ContextSource;
|
|
18
|
+
/** The served store when it is not hippo.db; then every read and write goes through it and no hippo.db opens. */
|
|
19
|
+
other: HippoStore | undefined;
|
|
10
20
|
pinnedOnly: boolean;
|
|
11
21
|
limit: number;
|
|
12
22
|
includeRecent: number;
|
|
@@ -241,7 +241,7 @@ export async function selectBySearch(ctx, plan, left, pools, admission) {
|
|
|
241
241
|
const results = plan.hasGlobal && !plan.primaryIsGlobal
|
|
242
242
|
? await searchBothStores(ctx, plan, left, minResults, pools, admission.bothStoresAdmit)
|
|
243
243
|
: await searchLocalRows(ctx, plan, left, minResults, pools.local.entries, admission.admit);
|
|
244
|
-
auditContextRecall(ctx, plan, results.length);
|
|
244
|
+
await auditContextRecall(ctx, plan, results.length);
|
|
245
245
|
return results;
|
|
246
246
|
}
|
|
247
247
|
// The pools were admitted at load, before ranking, dedupe and budget: a post-filter would let an excluded row fill the
|
|
@@ -273,6 +273,7 @@ async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit)
|
|
|
273
273
|
const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
|
|
274
274
|
const localCost = cost && ((r) => price(r.entry, primaryIsGlobal));
|
|
275
275
|
const vectorCandidates = contextVectorSpec(ctx, plan, admit);
|
|
276
|
+
const store = plan.local.store;
|
|
276
277
|
const ctxResults = usePhysicsCtx
|
|
277
278
|
? await physicsSearch(query, localEntries, {
|
|
278
279
|
budget: left,
|
|
@@ -282,6 +283,7 @@ async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit)
|
|
|
282
283
|
physicsConfig: ctxConfig.physics,
|
|
283
284
|
scope: plan.activeScope,
|
|
284
285
|
vectorCandidates,
|
|
286
|
+
store,
|
|
285
287
|
})
|
|
286
288
|
: await hybridSearch(query, localEntries, {
|
|
287
289
|
budget: left,
|
|
@@ -290,6 +292,7 @@ async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit)
|
|
|
290
292
|
hippoRoot: ctx.hippoRoot,
|
|
291
293
|
scope: plan.activeScope,
|
|
292
294
|
vectorCandidates,
|
|
295
|
+
store,
|
|
293
296
|
});
|
|
294
297
|
return ctxResults.map((r) => ({
|
|
295
298
|
entry: r.entry,
|
|
@@ -299,21 +302,21 @@ async function searchLocalRows(ctx, plan, left, minResults, localEntries, admit)
|
|
|
299
302
|
}));
|
|
300
303
|
}
|
|
301
304
|
// Same 'recall' op api.recall emits; the pinned-only and no-query branches never search, so they never emit.
|
|
302
|
-
function auditContextRecall(ctx, plan, resultCount) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
async function auditContextRecall(ctx, plan, resultCount) {
|
|
306
|
+
const row = {
|
|
307
|
+
tenantId: ctx.tenantId,
|
|
308
|
+
actor: ctx.actor.subject,
|
|
309
|
+
op: 'recall',
|
|
310
|
+
metadata: { ...auditQueryFields(plan.query), results: resultCount, mode: 'context' },
|
|
307
311
|
};
|
|
312
|
+
if (plan.other) {
|
|
313
|
+
await plan.other.appendAuditEvents([row]);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
308
316
|
if (plan.hasLocal) {
|
|
309
317
|
const localDb = openHippoDb(ctx.hippoRoot);
|
|
310
318
|
try {
|
|
311
|
-
appendAuditEvent(localDb,
|
|
312
|
-
tenantId: ctx.tenantId,
|
|
313
|
-
actor: ctx.actor.subject,
|
|
314
|
-
op: 'recall',
|
|
315
|
-
metadata: ctxRecallMetadata,
|
|
316
|
-
});
|
|
319
|
+
appendAuditEvent(localDb, row);
|
|
317
320
|
}
|
|
318
321
|
finally {
|
|
319
322
|
closeHippoDb(localDb);
|
|
@@ -322,12 +325,7 @@ function auditContextRecall(ctx, plan, resultCount) {
|
|
|
322
325
|
if (plan.hasGlobal && !plan.primaryIsGlobal) {
|
|
323
326
|
const globalDb = openHippoDb(plan.globalRoot);
|
|
324
327
|
try {
|
|
325
|
-
appendAuditEvent(globalDb,
|
|
326
|
-
tenantId: ctx.tenantId,
|
|
327
|
-
actor: ctx.actor.subject,
|
|
328
|
-
op: 'recall',
|
|
329
|
-
metadata: ctxRecallMetadata,
|
|
330
|
-
});
|
|
328
|
+
appendAuditEvent(globalDb, row);
|
|
331
329
|
}
|
|
332
330
|
finally {
|
|
333
331
|
closeHippoDb(globalDb);
|