@vellumai/assistant 0.5.3 → 0.5.5
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/Dockerfile +18 -27
- package/docs/architecture/memory.md +105 -0
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-types.test.ts +12 -4
- package/src/__tests__/memory-reducer.test.ts +7 -1
- package/src/__tests__/memory-regressions.test.ts +24 -4
- package/src/__tests__/memory-simplified-config.test.ts +4 -4
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +0 -1
- package/src/config/schemas/memory-simplified.ts +1 -1
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/config-watcher.ts +4 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/lifecycle.ts +51 -2
- package/src/daemon/providers-setup.ts +2 -1
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/brief-time.ts +5 -4
- package/src/memory/conversation-crud.ts +210 -0
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +24 -30
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/jobs-store.ts +2 -0
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-types.ts +9 -2
- package/src/memory/reducer.ts +25 -11
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/trust-client.ts +343 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +523 -36
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +88 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/secret-routes.ts +5 -1
- package/src/schedule/schedule-store.ts +7 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +22 -20
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety tests for DELETE /v1/conversations (clear all conversations).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Route policy requires `settings.write` scope (not just `chat.write`)
|
|
6
|
+
* - Missing X-Confirm-Destructive header returns 400 with explanatory message
|
|
7
|
+
* - Wrong header value returns 400
|
|
8
|
+
* - Correct scope + header clears data and returns 204
|
|
9
|
+
* - lifecycle_events contains `conversations_clear_all` audit entry after clear
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { afterAll, describe, expect, mock, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
const testDir = mkdtempSync(join(tmpdir(), "conv-clear-safety-test-"));
|
|
18
|
+
|
|
19
|
+
mock.module("../util/platform.js", () => ({
|
|
20
|
+
getRootDir: () => testDir,
|
|
21
|
+
getDataDir: () => testDir,
|
|
22
|
+
isMacOS: () => process.platform === "darwin",
|
|
23
|
+
isLinux: () => process.platform === "linux",
|
|
24
|
+
isWindows: () => process.platform === "win32",
|
|
25
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
26
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
27
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
28
|
+
ensureDataDir: () => {},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
mock.module("../util/logger.js", () => ({
|
|
32
|
+
getLogger: () =>
|
|
33
|
+
new Proxy({} as Record<string, unknown>, {
|
|
34
|
+
get: () => () => {},
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Auth is NOT disabled — we need enforcePolicy to actually check scopes.
|
|
39
|
+
let authDisabled = false;
|
|
40
|
+
mock.module("../config/env.js", () => ({
|
|
41
|
+
isHttpAuthDisabled: () => authDisabled,
|
|
42
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
addMessage,
|
|
47
|
+
clearAll,
|
|
48
|
+
createConversation,
|
|
49
|
+
getConversation,
|
|
50
|
+
} from "../memory/conversation-crud.js";
|
|
51
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
52
|
+
import { enforcePolicy, getPolicy } from "../runtime/auth/route-policy.js";
|
|
53
|
+
import type { AuthContext, Scope } from "../runtime/auth/types.js";
|
|
54
|
+
import { conversationManagementRouteDefinitions } from "../runtime/routes/conversation-management-routes.js";
|
|
55
|
+
|
|
56
|
+
initializeDb();
|
|
57
|
+
|
|
58
|
+
afterAll(() => {
|
|
59
|
+
resetDb();
|
|
60
|
+
try {
|
|
61
|
+
rmSync(testDir, { recursive: true });
|
|
62
|
+
} catch {
|
|
63
|
+
/* best effort */
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** Build a synthetic AuthContext for testing. */
|
|
68
|
+
function buildAuthContext(overrides?: {
|
|
69
|
+
principalType?: AuthContext["principalType"];
|
|
70
|
+
scopes?: Scope[];
|
|
71
|
+
}): AuthContext {
|
|
72
|
+
return {
|
|
73
|
+
subject: "actor:self:test-principal",
|
|
74
|
+
principalType: overrides?.principalType ?? "actor",
|
|
75
|
+
assistantId: "self",
|
|
76
|
+
actorPrincipalId: "test-principal",
|
|
77
|
+
scopeProfile: "actor_client_v1",
|
|
78
|
+
scopes: new Set(
|
|
79
|
+
overrides?.scopes ?? [
|
|
80
|
+
"chat.read",
|
|
81
|
+
"chat.write",
|
|
82
|
+
"approval.read",
|
|
83
|
+
"approval.write",
|
|
84
|
+
],
|
|
85
|
+
),
|
|
86
|
+
policyEpoch: 1,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Route policy tests (scope check — enforcePolicy level)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe("DELETE /v1/conversations — route policy", () => {
|
|
95
|
+
test("conversations/clear-all requires settings.write scope", () => {
|
|
96
|
+
authDisabled = false;
|
|
97
|
+
const policy = getPolicy("conversations/clear-all");
|
|
98
|
+
expect(policy).toBeDefined();
|
|
99
|
+
expect(policy!.requiredScopes).toContain("settings.write");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("chat.write-only token is rejected with 403 for clear-all", () => {
|
|
103
|
+
authDisabled = false;
|
|
104
|
+
const ctx = buildAuthContext({
|
|
105
|
+
scopes: ["chat.read", "chat.write"],
|
|
106
|
+
});
|
|
107
|
+
const result = enforcePolicy("conversations/clear-all", ctx);
|
|
108
|
+
expect(result).not.toBeNull();
|
|
109
|
+
expect(result!.status).toBe(403);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("settings.write token is allowed through clear-all policy", () => {
|
|
113
|
+
authDisabled = false;
|
|
114
|
+
const ctx = buildAuthContext({
|
|
115
|
+
scopes: ["settings.write"],
|
|
116
|
+
});
|
|
117
|
+
const result = enforcePolicy("conversations/clear-all", ctx);
|
|
118
|
+
expect(result).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("single-conversation DELETE (conversations:DELETE) only requires chat.write", () => {
|
|
122
|
+
authDisabled = false;
|
|
123
|
+
const policy = getPolicy("conversations:DELETE");
|
|
124
|
+
expect(policy).toBeDefined();
|
|
125
|
+
expect(policy!.requiredScopes).toContain("chat.write");
|
|
126
|
+
expect(policy!.requiredScopes).not.toContain("settings.write");
|
|
127
|
+
|
|
128
|
+
// A chat.write token should pass the single-conversation delete policy
|
|
129
|
+
const ctx = buildAuthContext({
|
|
130
|
+
scopes: ["chat.read", "chat.write"],
|
|
131
|
+
});
|
|
132
|
+
const result = enforcePolicy("conversations:DELETE", ctx);
|
|
133
|
+
expect(result).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Route handler tests (header check + happy path)
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
describe("DELETE /v1/conversations — route handler", () => {
|
|
142
|
+
/** Get the DELETE conversations handler from the route definitions. */
|
|
143
|
+
function getDeleteHandler() {
|
|
144
|
+
let clearCalled = false;
|
|
145
|
+
const routes = conversationManagementRouteDefinitions({
|
|
146
|
+
switchConversation: async () => null,
|
|
147
|
+
renameConversation: () => true,
|
|
148
|
+
clearAllConversations: () => {
|
|
149
|
+
clearCalled = true;
|
|
150
|
+
return clearAll().conversations;
|
|
151
|
+
},
|
|
152
|
+
cancelGeneration: () => true,
|
|
153
|
+
destroyConversation: () => {},
|
|
154
|
+
undoLastMessage: async () => null,
|
|
155
|
+
regenerateResponse: async () => null,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const deleteRoute = routes.find(
|
|
159
|
+
(r) => r.endpoint === "conversations" && r.method === "DELETE",
|
|
160
|
+
);
|
|
161
|
+
if (!deleteRoute) throw new Error("DELETE conversations route not found");
|
|
162
|
+
return { handler: deleteRoute.handler, wasClearCalled: () => clearCalled };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test("missing X-Confirm-Destructive header returns 400 with explanatory message", async () => {
|
|
166
|
+
const { handler } = getDeleteHandler();
|
|
167
|
+
const req = new Request("http://localhost/v1/conversations", {
|
|
168
|
+
method: "DELETE",
|
|
169
|
+
});
|
|
170
|
+
const response = await handler({
|
|
171
|
+
req,
|
|
172
|
+
url: new URL(req.url),
|
|
173
|
+
server: {} as never,
|
|
174
|
+
authContext: buildAuthContext({ scopes: ["settings.write"] }),
|
|
175
|
+
params: {},
|
|
176
|
+
});
|
|
177
|
+
expect(response.status).toBe(400);
|
|
178
|
+
const body = (await response.json()) as {
|
|
179
|
+
error: { code: string; message: string };
|
|
180
|
+
};
|
|
181
|
+
expect(body.error.code).toBe("BAD_REQUEST");
|
|
182
|
+
expect(body.error.message).toContain("X-Confirm-Destructive");
|
|
183
|
+
expect(body.error.message).toContain("clear-all-conversations");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("wrong X-Confirm-Destructive header value returns 400", async () => {
|
|
187
|
+
const { handler } = getDeleteHandler();
|
|
188
|
+
const req = new Request("http://localhost/v1/conversations", {
|
|
189
|
+
method: "DELETE",
|
|
190
|
+
headers: { "X-Confirm-Destructive": "wrong-value" },
|
|
191
|
+
});
|
|
192
|
+
const response = await handler({
|
|
193
|
+
req,
|
|
194
|
+
url: new URL(req.url),
|
|
195
|
+
server: {} as never,
|
|
196
|
+
authContext: buildAuthContext({ scopes: ["settings.write"] }),
|
|
197
|
+
params: {},
|
|
198
|
+
});
|
|
199
|
+
expect(response.status).toBe(400);
|
|
200
|
+
const body = (await response.json()) as {
|
|
201
|
+
error: { code: string; message: string };
|
|
202
|
+
};
|
|
203
|
+
expect(body.error.code).toBe("BAD_REQUEST");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("correct scope + header clears data and returns 204", async () => {
|
|
207
|
+
// Seed a conversation so we can verify it gets cleared
|
|
208
|
+
const conv = createConversation("safety-test-conv");
|
|
209
|
+
await addMessage(conv.id, "user", "hello from safety test");
|
|
210
|
+
expect(getConversation(conv.id)).not.toBeNull();
|
|
211
|
+
|
|
212
|
+
const { handler, wasClearCalled } = getDeleteHandler();
|
|
213
|
+
const req = new Request("http://localhost/v1/conversations", {
|
|
214
|
+
method: "DELETE",
|
|
215
|
+
headers: { "X-Confirm-Destructive": "clear-all-conversations" },
|
|
216
|
+
});
|
|
217
|
+
const response = await handler({
|
|
218
|
+
req,
|
|
219
|
+
url: new URL(req.url),
|
|
220
|
+
server: {} as never,
|
|
221
|
+
authContext: buildAuthContext({ scopes: ["settings.write"] }),
|
|
222
|
+
params: {},
|
|
223
|
+
});
|
|
224
|
+
expect(response.status).toBe(204);
|
|
225
|
+
expect(wasClearCalled()).toBe(true);
|
|
226
|
+
|
|
227
|
+
// Conversation should be gone
|
|
228
|
+
expect(getConversation(conv.id)).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("lifecycle_events contains conversations_clear_all after successful clear", async () => {
|
|
232
|
+
const { handler } = getDeleteHandler();
|
|
233
|
+
const req = new Request("http://localhost/v1/conversations", {
|
|
234
|
+
method: "DELETE",
|
|
235
|
+
headers: { "X-Confirm-Destructive": "clear-all-conversations" },
|
|
236
|
+
});
|
|
237
|
+
await handler({
|
|
238
|
+
req,
|
|
239
|
+
url: new URL(req.url),
|
|
240
|
+
server: {} as never,
|
|
241
|
+
authContext: buildAuthContext({ scopes: ["settings.write"] }),
|
|
242
|
+
params: {},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Query lifecycle_events table directly
|
|
246
|
+
const raw = (
|
|
247
|
+
getDb() as unknown as {
|
|
248
|
+
$client: import("bun:sqlite").Database;
|
|
249
|
+
}
|
|
250
|
+
).$client;
|
|
251
|
+
const rows = raw
|
|
252
|
+
.query(
|
|
253
|
+
"SELECT event_name FROM lifecycle_events WHERE event_name = 'conversations_clear_all'",
|
|
254
|
+
)
|
|
255
|
+
.all() as Array<{ event_name: string }>;
|
|
256
|
+
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
257
|
+
expect(rows[0].event_name).toBe("conversations_clear_all");
|
|
258
|
+
});
|
|
259
|
+
});
|