@vellumai/assistant 0.5.14 → 0.5.16
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/ARCHITECTURE.md +2 -2
- package/docs/architecture/integrations.md +15 -14
- package/knip.json +3 -1
- package/openapi.yaml +11 -43
- package/package.json +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
- package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
- package/src/__tests__/checker.test.ts +59 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
- package/src/__tests__/cli-memory.test.ts +372 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
- package/src/__tests__/config-schema.test.ts +0 -2
- package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +4 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
- package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -3
- package/src/__tests__/intent-routing.test.ts +6 -18
- package/src/__tests__/log-export-workspace.test.ts +2 -28
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
- package/src/__tests__/managed-store.test.ts +2 -10
- package/src/__tests__/messaging-send-tool.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
- package/src/__tests__/migration-export-http.test.ts +3 -34
- package/src/__tests__/migration-import-commit-http.test.ts +1 -29
- package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
- package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
- package/src/__tests__/oauth-apps-routes.test.ts +120 -10
- package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
- package/src/__tests__/oauth-providers-routes.test.ts +5 -2
- package/src/__tests__/oauth-store.test.ts +0 -5
- package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
- package/src/__tests__/path-policy.test.ts +2 -17
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/platform-callback-registration.test.ts +3 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -2
- package/src/__tests__/qdrant-manager.test.ts +68 -21
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
- package/src/__tests__/secret-allowlist.test.ts +20 -35
- package/src/__tests__/shell-credential-ref.test.ts +0 -5
- package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
- package/src/__tests__/skill-load-inline-command.test.ts +3 -65
- package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
- package/src/__tests__/skill-load-tool.test.ts +3 -67
- package/src/__tests__/skill-memory.test.ts +362 -119
- package/src/__tests__/skills.test.ts +22 -49
- package/src/__tests__/slack-channel-config.test.ts +2 -21
- package/src/__tests__/starter-bundle.test.ts +2 -8
- package/src/__tests__/stt-hints.test.ts +7 -2
- package/src/__tests__/system-prompt.test.ts +25 -45
- package/src/__tests__/task-compiler.test.ts +0 -21
- package/src/__tests__/task-management-tools.test.ts +0 -21
- package/src/__tests__/task-memory-cleanup.test.ts +0 -21
- package/src/__tests__/task-runner.test.ts +0 -21
- package/src/__tests__/task-scheduler.test.ts +0 -21
- package/src/__tests__/terminal-tools.test.ts +1 -17
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
- package/src/__tests__/tool-approval-handler.test.ts +1 -20
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
- package/src/__tests__/trust-store.test.ts +9 -41
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
- package/src/__tests__/trusted-contact-verification.test.ts +0 -22
- package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
- package/src/__tests__/twilio-provider.test.ts +0 -16
- package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
- package/src/__tests__/twilio-routes.test.ts +0 -24
- package/src/__tests__/update-bulletin.test.ts +17 -89
- package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
- package/src/__tests__/usage-routes.test.ts +0 -21
- package/src/__tests__/user-reference.test.ts +1 -5
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
- package/src/__tests__/voice-invite-redemption.test.ts +0 -21
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
- package/src/__tests__/voice-session-bridge.test.ts +0 -21
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
- package/src/acp/client-handler.ts +1 -2
- package/src/cli/__tests__/notifications.test.ts +0 -22
- package/src/cli/cli-memory.ts +176 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +15 -0
- package/src/cli/commands/oauth/providers.ts +49 -42
- package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
- package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/context/window-manager.ts +28 -9
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/daemon/config-watcher.ts +51 -0
- package/src/daemon/conversation-agent-loop.ts +3 -2
- package/src/daemon/conversation-process.ts +1 -0
- package/src/daemon/conversation-usage.ts +1 -0
- package/src/daemon/handlers/skills.ts +9 -1
- package/src/daemon/lifecycle.ts +13 -4
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/providers-setup.ts +2 -0
- package/src/daemon/server.ts +26 -22
- package/src/events/domain-events.ts +1 -2
- package/src/memory/db-init.ts +9 -0
- package/src/memory/job-handlers/batch-extraction.ts +16 -4
- package/src/memory/job-handlers/embedding.test.ts +3 -27
- package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
- package/src/memory/llm-usage-store.ts +35 -2
- package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
- package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/qdrant-manager.ts +26 -5
- package/src/memory/query-expansion.ts +1 -1
- package/src/memory/retriever.test.ts +22 -20
- package/src/memory/retriever.ts +10 -2
- package/src/memory/schema/oauth.ts +1 -1
- package/src/memory/search/mmr.ts +8 -5
- package/src/memory/slack-thread-store.ts +17 -0
- package/src/messaging/providers/outlook/adapter.ts +193 -0
- package/src/messaging/providers/outlook/client.ts +311 -0
- package/src/messaging/providers/outlook/types.ts +83 -0
- package/src/notifications/adapters/slack.ts +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +10 -3
- package/src/oauth/oauth-store.ts +10 -11
- package/src/oauth/provider-serializer.ts +3 -0
- package/src/oauth/provider-visibility.ts +16 -0
- package/src/oauth/seed-providers.ts +49 -17
- package/src/permissions/checker.ts +39 -7
- package/src/permissions/types.ts +2 -4
- package/src/prompts/journal-context.ts +9 -11
- package/src/prompts/system-prompt.ts +3 -64
- package/src/prompts/templates/UPDATES.md +6 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
- package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/routes/conversation-query-routes.ts +2 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
- package/src/runtime/routes/memory-item-routes.test.ts +0 -17
- package/src/runtime/routes/memory-item-routes.ts +103 -12
- package/src/runtime/routes/oauth-apps.ts +18 -1
- package/src/runtime/routes/oauth-providers.ts +13 -1
- package/src/runtime/routes/settings-routes.ts +1 -0
- package/src/runtime/routes/usage-routes.ts +19 -2
- package/src/runtime/routes/work-items-routes.test.ts +0 -21
- package/src/runtime/routes/workspace-routes.test.ts +3 -27
- package/src/security/secret-allowlist.ts +4 -4
- package/src/skills/skill-memory.ts +62 -23
- package/src/tools/memory/handlers.test.ts +1 -29
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/skills/skill-script-runner.ts +1 -1
- package/src/util/device-id.ts +3 -65
- package/src/workspace/git-service.ts +27 -6
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
-
|
|
6
|
-
const testDir = realpathSync(mkdtempSync(join(tmpdir(), "auth-token-test-")));
|
|
7
|
-
|
|
8
|
-
mock.module("../../../util/platform.js", () => ({
|
|
9
|
-
getRootDir: () => testDir,
|
|
10
|
-
getDataDir: () => testDir,
|
|
11
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
12
|
-
normalizeAssistantId: (id: string) => (id === "self" ? "self" : id),
|
|
13
|
-
isMacOS: () => process.platform === "darwin",
|
|
14
|
-
isLinux: () => process.platform === "linux",
|
|
15
|
-
isWindows: () => process.platform === "win32",
|
|
16
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
17
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
18
|
-
ensureDataDir: () => {},
|
|
19
|
-
}));
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
20
2
|
|
|
21
3
|
mock.module("../../../util/logger.js", () => ({
|
|
22
4
|
getLogger: () =>
|
|
@@ -39,12 +21,6 @@ beforeEach(() => {
|
|
|
39
21
|
initAuthSigningKey(TEST_KEY);
|
|
40
22
|
});
|
|
41
23
|
|
|
42
|
-
afterAll(() => {
|
|
43
|
-
try {
|
|
44
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
45
|
-
} catch {}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
24
|
describe("mintToken / verifyToken round-trip", () => {
|
|
49
25
|
test("mint + verify succeeds for valid token targeting vellum-daemon", () => {
|
|
50
26
|
const token = mintToken({
|
|
@@ -349,10 +349,6 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
349
349
|
{ endpoint: "config/embeddings:GET", scopes: ["settings.read"] },
|
|
350
350
|
{ endpoint: "config/embeddings:PUT", scopes: ["settings.write"] },
|
|
351
351
|
|
|
352
|
-
// Permissions config
|
|
353
|
-
{ endpoint: "config/permissions/skip:GET", scopes: ["settings.read"] },
|
|
354
|
-
{ endpoint: "config/permissions/skip:PUT", scopes: ["settings.write"] },
|
|
355
|
-
|
|
356
352
|
// Generic config read/patch
|
|
357
353
|
{ endpoint: "config:GET", scopes: ["settings.read"] },
|
|
358
354
|
{ endpoint: "config:PATCH", scopes: ["settings.write"] },
|
|
@@ -155,8 +155,11 @@ function parseRequestCode(
|
|
|
155
155
|
text: string,
|
|
156
156
|
scopeConversationId?: string,
|
|
157
157
|
): CodeParseResult | null {
|
|
158
|
+
// Strip common channel formatting delimiters (backticks, bold, italic,
|
|
159
|
+
// strikethrough) that messaging platforms wrap around inline code.
|
|
160
|
+
const cleaned = text.replace(/^[`*_~]+/, "").replace(/[`*_~]+$/, "").trim();
|
|
158
161
|
// Request codes are 6 hex chars (A-F, 0-9), uppercase
|
|
159
|
-
const upper =
|
|
162
|
+
const upper = cleaned.toUpperCase();
|
|
160
163
|
const match = upper.match(/^([A-F0-9]{6})(?:\s|$)/);
|
|
161
164
|
if (!match) return null;
|
|
162
165
|
|
|
@@ -185,7 +188,7 @@ function parseRequestCode(
|
|
|
185
188
|
return null;
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
const remainingText =
|
|
191
|
+
const remainingText = cleaned.slice(code.length).trim();
|
|
189
192
|
return { request, remainingText };
|
|
190
193
|
}
|
|
191
194
|
|
|
@@ -754,6 +757,7 @@ const EXPLICIT_REJECT_PHRASES: ReadonlySet<string> = new Set([
|
|
|
754
757
|
|
|
755
758
|
function normalizeDecisionPhrase(text: string): string {
|
|
756
759
|
return text
|
|
760
|
+
.replace(/[`*_~]/g, "")
|
|
757
761
|
.trim()
|
|
758
762
|
.toLowerCase()
|
|
759
763
|
.replace(/[.!?]+$/g, "")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP route definitions for model configuration, embedding configuration,
|
|
3
|
-
*
|
|
3
|
+
* conversation search, message content, LLM
|
|
4
4
|
* context inspection, and queued message deletion.
|
|
5
5
|
*
|
|
6
6
|
* These routes expose conversation query functionality over the HTTP API.
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
* PUT /v1/model/image-gen — set image-gen model
|
|
11
11
|
* GET /v1/config/embeddings — current embedding config
|
|
12
12
|
* PUT /v1/config/embeddings — set embedding provider/model
|
|
13
|
-
* GET /v1/config/permissions/skip — dangerouslySkipPermissions status
|
|
14
|
-
* PUT /v1/config/permissions/skip — toggle dangerouslySkipPermissions
|
|
15
13
|
* GET /v1/config — full raw workspace config
|
|
16
14
|
* PATCH /v1/config — deep-merge partial config
|
|
17
15
|
* GET /v1/conversations/search — search conversations
|
|
@@ -24,7 +22,6 @@ import { z } from "zod";
|
|
|
24
22
|
|
|
25
23
|
import {
|
|
26
24
|
deepMergeOverwrite,
|
|
27
|
-
getConfig,
|
|
28
25
|
loadRawConfig,
|
|
29
26
|
saveRawConfig,
|
|
30
27
|
} from "../../config/loader.js";
|
|
@@ -113,9 +110,7 @@ function applyStoredProviderToLlmContextResult(
|
|
|
113
110
|
const mergedSummary = normalized.summary
|
|
114
111
|
? { ...normalized.summary, provider }
|
|
115
112
|
: { provider };
|
|
116
|
-
const summary = attachEstimatedCost(
|
|
117
|
-
mergedSummary as LlmContextSummary,
|
|
118
|
-
);
|
|
113
|
+
const summary = attachEstimatedCost(mergedSummary as LlmContextSummary);
|
|
119
114
|
return { ...normalized, summary };
|
|
120
115
|
}
|
|
121
116
|
|
|
@@ -324,57 +319,6 @@ export function conversationQueryRouteDefinitions(
|
|
|
324
319
|
},
|
|
325
320
|
},
|
|
326
321
|
|
|
327
|
-
// ── Permissions config ─────────────────────────────────────────────
|
|
328
|
-
{
|
|
329
|
-
endpoint: "config/permissions/skip",
|
|
330
|
-
method: "GET",
|
|
331
|
-
policyKey: "config/permissions/skip",
|
|
332
|
-
summary: "Get permission-skip flag",
|
|
333
|
-
description: "Return whether dangerouslySkipPermissions is enabled.",
|
|
334
|
-
tags: ["config"],
|
|
335
|
-
responseBody: z.object({
|
|
336
|
-
enabled: z.boolean(),
|
|
337
|
-
}),
|
|
338
|
-
handler: () => {
|
|
339
|
-
const config = getConfig();
|
|
340
|
-
return Response.json({
|
|
341
|
-
enabled: config.permissions.dangerouslySkipPermissions,
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
endpoint: "config/permissions/skip",
|
|
347
|
-
method: "PUT",
|
|
348
|
-
policyKey: "config/permissions/skip",
|
|
349
|
-
summary: "Set permission-skip flag",
|
|
350
|
-
description: "Enable or disable dangerouslySkipPermissions.",
|
|
351
|
-
tags: ["config"],
|
|
352
|
-
requestBody: z.object({
|
|
353
|
-
enabled: z.boolean(),
|
|
354
|
-
}),
|
|
355
|
-
handler: async ({ req }) => {
|
|
356
|
-
const body = (await req.json()) as { enabled?: unknown };
|
|
357
|
-
if (typeof body.enabled !== "boolean") {
|
|
358
|
-
return httpError(
|
|
359
|
-
"BAD_REQUEST",
|
|
360
|
-
"Missing or invalid field: enabled (boolean)",
|
|
361
|
-
400,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
const raw = loadRawConfig();
|
|
365
|
-
const permissions: Record<string, unknown> =
|
|
366
|
-
raw.permissions != null &&
|
|
367
|
-
typeof raw.permissions === "object" &&
|
|
368
|
-
!Array.isArray(raw.permissions)
|
|
369
|
-
? (raw.permissions as Record<string, unknown>)
|
|
370
|
-
: {};
|
|
371
|
-
permissions.dangerouslySkipPermissions = body.enabled;
|
|
372
|
-
raw.permissions = permissions;
|
|
373
|
-
saveRawConfig(raw);
|
|
374
|
-
return Response.json({ enabled: body.enabled });
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
|
|
378
322
|
// ── Full config read ─────────────────────────────────────────────
|
|
379
323
|
{
|
|
380
324
|
endpoint: "config",
|
|
@@ -15,6 +15,7 @@ import * as deliveryCrud from "../../../memory/delivery-crud.js";
|
|
|
15
15
|
import * as deliveryStatus from "../../../memory/delivery-status.js";
|
|
16
16
|
import {
|
|
17
17
|
extractChannelFromCallbackUrl,
|
|
18
|
+
extractMessageTsFromCallbackUrl,
|
|
18
19
|
extractThreadTsFromCallbackUrl,
|
|
19
20
|
setThreadTs,
|
|
20
21
|
} from "../../../memory/slack-thread-store.js";
|
|
@@ -328,9 +329,49 @@ export function setSlackThinkingStatus(
|
|
|
328
329
|
// the correct thread for the Assistants API status.
|
|
329
330
|
const threadTs = extractThreadTsFromCallbackUrl(callbackUrl);
|
|
330
331
|
|
|
331
|
-
//
|
|
332
|
+
// For non-threaded DMs, fall back to emoji reaction on the original message.
|
|
332
333
|
if (!threadTs) {
|
|
333
|
-
|
|
334
|
+
const messageTs = extractMessageTsFromCallbackUrl(callbackUrl);
|
|
335
|
+
if (!messageTs) return () => {};
|
|
336
|
+
|
|
337
|
+
const addPromise = deliverChannelReply(
|
|
338
|
+
callbackUrl,
|
|
339
|
+
{
|
|
340
|
+
chatId,
|
|
341
|
+
assistantId,
|
|
342
|
+
reaction: { action: "add", name: "eyes", messageTs },
|
|
343
|
+
},
|
|
344
|
+
mintBearerToken(),
|
|
345
|
+
).catch((err) => {
|
|
346
|
+
log.debug({ err, chatId, messageTs }, "Failed to add Slack eyes reaction");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const clearReaction = () => {
|
|
350
|
+
if (cleared) return;
|
|
351
|
+
cleared = true;
|
|
352
|
+
clearTimeout(safetyTimer);
|
|
353
|
+
void addPromise.then(() =>
|
|
354
|
+
deliverChannelReply(
|
|
355
|
+
callbackUrl,
|
|
356
|
+
{
|
|
357
|
+
chatId,
|
|
358
|
+
assistantId,
|
|
359
|
+
reaction: { action: "remove", name: "eyes", messageTs },
|
|
360
|
+
},
|
|
361
|
+
mintBearerToken(),
|
|
362
|
+
).catch((err) => {
|
|
363
|
+
log.debug(
|
|
364
|
+
{ err, chatId, messageTs },
|
|
365
|
+
"Failed to remove Slack eyes reaction",
|
|
366
|
+
);
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const safetyTimer = setTimeout(clearReaction, SLACK_THINKING_MAX_DURATION_MS);
|
|
372
|
+
(safetyTimer as { unref?: () => void }).unref?.();
|
|
373
|
+
|
|
374
|
+
return clearReaction;
|
|
334
375
|
}
|
|
335
376
|
|
|
336
377
|
// Track the set promise so clear waits for it to settle first,
|
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
* Covers: list with filters, get by ID, create + duplicate rejection,
|
|
5
5
|
* update + fingerprint collision, delete + 404.
|
|
6
6
|
*/
|
|
7
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
7
|
import {
|
|
11
8
|
afterAll,
|
|
12
9
|
beforeAll,
|
|
@@ -17,19 +14,6 @@ import {
|
|
|
17
14
|
test,
|
|
18
15
|
} from "bun:test";
|
|
19
16
|
|
|
20
|
-
const testDir = mkdtempSync(join(tmpdir(), "memory-item-routes-test-"));
|
|
21
|
-
|
|
22
|
-
mock.module("../../util/platform.js", () => ({
|
|
23
|
-
getDataDir: () => testDir,
|
|
24
|
-
isMacOS: () => process.platform === "darwin",
|
|
25
|
-
isLinux: () => process.platform === "linux",
|
|
26
|
-
isWindows: () => process.platform === "win32",
|
|
27
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
28
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
29
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
30
|
-
ensureDataDir: () => {},
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
17
|
mock.module("../../util/logger.js", () => ({
|
|
34
18
|
getLogger: () =>
|
|
35
19
|
new Proxy({} as Record<string, unknown>, {
|
|
@@ -206,7 +190,6 @@ describe("Memory Item Routes", () => {
|
|
|
206
190
|
|
|
207
191
|
afterAll(() => {
|
|
208
192
|
resetDb();
|
|
209
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
210
193
|
});
|
|
211
194
|
|
|
212
195
|
// =========================================================================
|
|
@@ -129,6 +129,26 @@ function buildConversationTitleMap(
|
|
|
129
129
|
return map;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Semantic search constants
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Maximum number of candidate IDs fetched from Qdrant for semantic search.
|
|
138
|
+
*
|
|
139
|
+
* Kind-filtering and pagination happen *after* this fetch, so if a broad query
|
|
140
|
+
* matches more items than this ceiling, results beyond it are silently excluded
|
|
141
|
+
* — kind-filtered counts may be under-reported and offsets past this value are
|
|
142
|
+
* unreachable. The limit exists to bound Qdrant query cost and the size of the
|
|
143
|
+
* subsequent `IN (...)` SQL clauses (SQLite's SQLITE_MAX_VARIABLE_NUMBER is
|
|
144
|
+
* 32,766 in Bun's bundled build, so 10k stays well within that).
|
|
145
|
+
*
|
|
146
|
+
* If this becomes a real bottleneck for large memory stores, consider pushing
|
|
147
|
+
* kind/status filters into Qdrant payload filtering so the fetch limit only
|
|
148
|
+
* needs to cover the final result set.
|
|
149
|
+
*/
|
|
150
|
+
const SEMANTIC_SEARCH_FETCH_CEILING = 10_000;
|
|
151
|
+
|
|
132
152
|
// ---------------------------------------------------------------------------
|
|
133
153
|
// Semantic search helper
|
|
134
154
|
// ---------------------------------------------------------------------------
|
|
@@ -239,26 +259,68 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
|
|
|
239
259
|
// When a search query is present, try Qdrant hybrid search first.
|
|
240
260
|
// Falls back to SQL LIKE when embeddings / Qdrant are unavailable.
|
|
241
261
|
if (searchParam) {
|
|
262
|
+
// Search WITHOUT kind filter so we can compute cross-kind counts.
|
|
263
|
+
// Kind filtering is applied post-hoc while preserving relevance order.
|
|
242
264
|
const semanticResult = await searchItemsSemantic(
|
|
243
265
|
searchParam,
|
|
244
|
-
|
|
245
|
-
|
|
266
|
+
SEMANTIC_SEARCH_FETCH_CEILING,
|
|
267
|
+
null,
|
|
246
268
|
statusParam,
|
|
247
269
|
);
|
|
248
270
|
|
|
249
271
|
if (semanticResult && semanticResult.ids.length > 0) {
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
272
|
+
// Compute kindCounts from all semantic matches (no kind filter).
|
|
273
|
+
// Status filter is applied as defense-in-depth against stale Qdrant payloads.
|
|
274
|
+
const kindCountConditions = [inArray(memoryItems.id, semanticResult.ids)];
|
|
275
|
+
if (statusParam && statusParam !== "all") {
|
|
276
|
+
kindCountConditions.push(eq(memoryItems.status, statusParam));
|
|
277
|
+
}
|
|
278
|
+
const kindCountRows = db
|
|
279
|
+
.select({ kind: memoryItems.kind, count: count() })
|
|
280
|
+
.from(memoryItems)
|
|
281
|
+
.where(and(...kindCountConditions))
|
|
282
|
+
.groupBy(memoryItems.kind)
|
|
283
|
+
.all();
|
|
284
|
+
const semanticKindCounts: Record<string, number> = {};
|
|
285
|
+
for (const row of kindCountRows) {
|
|
286
|
+
semanticKindCounts[row.kind] = row.count;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Apply kind filter while preserving semantic relevance ordering.
|
|
290
|
+
// Status filter is applied here too for defense-in-depth consistency.
|
|
291
|
+
let filteredIds = semanticResult.ids;
|
|
292
|
+
if (kindParam) {
|
|
293
|
+
const kindFilterConditions = [
|
|
294
|
+
inArray(memoryItems.id, semanticResult.ids),
|
|
295
|
+
eq(memoryItems.kind, kindParam),
|
|
296
|
+
];
|
|
297
|
+
if (statusParam && statusParam !== "all") {
|
|
298
|
+
kindFilterConditions.push(eq(memoryItems.status, statusParam));
|
|
299
|
+
}
|
|
300
|
+
const kindIdSet = new Set(
|
|
301
|
+
db
|
|
302
|
+
.select({ id: memoryItems.id })
|
|
303
|
+
.from(memoryItems)
|
|
304
|
+
.where(and(...kindFilterConditions))
|
|
305
|
+
.all()
|
|
306
|
+
.map((r) => r.id),
|
|
307
|
+
);
|
|
308
|
+
filteredIds = semanticResult.ids.filter((id) => kindIdSet.has(id));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const total = filteredIds.length;
|
|
312
|
+
const pageIds = filteredIds.slice(offsetParam, offsetParam + limitParam);
|
|
255
313
|
|
|
256
314
|
if (pageIds.length === 0) {
|
|
257
|
-
return Response.json({
|
|
315
|
+
return Response.json({
|
|
316
|
+
items: [],
|
|
317
|
+
total,
|
|
318
|
+
kindCounts: semanticKindCounts,
|
|
319
|
+
});
|
|
258
320
|
}
|
|
259
321
|
|
|
260
|
-
// Re-apply
|
|
261
|
-
//
|
|
322
|
+
// Re-apply DB-side filters as defense-in-depth against stale Qdrant
|
|
323
|
+
// payloads leaking deleted/mismatched rows.
|
|
262
324
|
const hydrationConditions = [inArray(memoryItems.id, pageIds)];
|
|
263
325
|
if (statusParam && statusParam !== "all") {
|
|
264
326
|
hydrationConditions.push(eq(memoryItems.status, statusParam));
|
|
@@ -288,12 +350,41 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
|
|
|
288
350
|
|
|
289
351
|
return Response.json({
|
|
290
352
|
items: enrichedItems,
|
|
291
|
-
total
|
|
353
|
+
total,
|
|
354
|
+
kindCounts: semanticKindCounts,
|
|
292
355
|
});
|
|
293
356
|
}
|
|
294
357
|
// semanticResult was null (Qdrant unavailable) or empty — fall through to SQL
|
|
295
358
|
}
|
|
296
359
|
|
|
360
|
+
// ── Kind counts for SQL path ───────────────────────────────────────
|
|
361
|
+
// Respects status/search filters but NOT kind filter, so the sidebar
|
|
362
|
+
// can show totals for every kind simultaneously.
|
|
363
|
+
const kindCountConditions = [];
|
|
364
|
+
if (statusParam && statusParam !== "all") {
|
|
365
|
+
kindCountConditions.push(eq(memoryItems.status, statusParam));
|
|
366
|
+
}
|
|
367
|
+
if (searchParam) {
|
|
368
|
+
kindCountConditions.push(
|
|
369
|
+
or(
|
|
370
|
+
like(memoryItems.subject, `%${searchParam}%`),
|
|
371
|
+
like(memoryItems.statement, `%${searchParam}%`),
|
|
372
|
+
)!,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const kindCountWhere =
|
|
376
|
+
kindCountConditions.length > 0 ? and(...kindCountConditions) : undefined;
|
|
377
|
+
const sqlKindCountRows = db
|
|
378
|
+
.select({ kind: memoryItems.kind, count: count() })
|
|
379
|
+
.from(memoryItems)
|
|
380
|
+
.where(kindCountWhere)
|
|
381
|
+
.groupBy(memoryItems.kind)
|
|
382
|
+
.all();
|
|
383
|
+
const kindCounts: Record<string, number> = {};
|
|
384
|
+
for (const row of sqlKindCountRows) {
|
|
385
|
+
kindCounts[row.kind] = row.count;
|
|
386
|
+
}
|
|
387
|
+
|
|
297
388
|
// ── SQL path (default or fallback) ──────────────────────────────────
|
|
298
389
|
const conditions = [];
|
|
299
390
|
if (statusParam && statusParam !== "all") {
|
|
@@ -344,7 +435,7 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
|
|
|
344
435
|
scopeLabel: resolveScopeLabel(item.scopeId, titleMap),
|
|
345
436
|
}));
|
|
346
437
|
|
|
347
|
-
return Response.json({ items: enrichedItems, total });
|
|
438
|
+
return Response.json({ items: enrichedItems, total, kindCounts });
|
|
348
439
|
}
|
|
349
440
|
|
|
350
441
|
// ---------------------------------------------------------------------------
|
|
@@ -253,7 +253,10 @@ export function oauthAppsRouteDefinitions(): RouteDefinition[] {
|
|
|
253
253
|
);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
let body: {
|
|
256
|
+
let body: {
|
|
257
|
+
scopes?: string[];
|
|
258
|
+
callback_transport?: "loopback" | "gateway";
|
|
259
|
+
} = {};
|
|
257
260
|
try {
|
|
258
261
|
const text = await req.text();
|
|
259
262
|
if (text) {
|
|
@@ -263,6 +266,19 @@ export function oauthAppsRouteDefinitions(): RouteDefinition[] {
|
|
|
263
266
|
// No body or invalid JSON — use defaults
|
|
264
267
|
}
|
|
265
268
|
|
|
269
|
+
// Validate callback_transport if present
|
|
270
|
+
if (
|
|
271
|
+
body.callback_transport !== undefined &&
|
|
272
|
+
body.callback_transport !== "loopback" &&
|
|
273
|
+
body.callback_transport !== "gateway"
|
|
274
|
+
) {
|
|
275
|
+
return httpError(
|
|
276
|
+
"BAD_REQUEST",
|
|
277
|
+
'callback_transport must be "loopback" or "gateway"',
|
|
278
|
+
400,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
266
282
|
const clientSecret = await getAppClientSecret(app);
|
|
267
283
|
|
|
268
284
|
const result = await orchestrateOAuthConnect({
|
|
@@ -270,6 +286,7 @@ export function oauthAppsRouteDefinitions(): RouteDefinition[] {
|
|
|
270
286
|
clientId: app.clientId,
|
|
271
287
|
clientSecret,
|
|
272
288
|
requestedScopes: body.scopes,
|
|
289
|
+
callbackTransport: body.callback_transport ?? "loopback",
|
|
273
290
|
isInteractive: false,
|
|
274
291
|
});
|
|
275
292
|
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* runtime auth middleware.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { loadConfig } from "../../config/loader.js";
|
|
9
10
|
import { getProvider, listProviders } from "../../oauth/oauth-store.js";
|
|
10
11
|
import { serializeProviderSummary } from "../../oauth/provider-serializer.js";
|
|
12
|
+
import { isProviderVisible } from "../../oauth/provider-visibility.js";
|
|
11
13
|
import { httpError } from "../http-errors.js";
|
|
12
14
|
import type { RouteDefinition } from "../http-router.js";
|
|
13
15
|
|
|
@@ -22,7 +24,9 @@ export function oauthProvidersRouteDefinitions(): RouteDefinition[] {
|
|
|
22
24
|
method: "GET",
|
|
23
25
|
handler: ({ url }) => {
|
|
24
26
|
const rows = listProviders();
|
|
25
|
-
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
const visibleRows = rows.filter((r) => isProviderVisible(r, config));
|
|
29
|
+
let serialized = visibleRows
|
|
26
30
|
.map((row) => serializeProviderSummary(row))
|
|
27
31
|
.filter((s): s is NonNullable<typeof s> => s !== null);
|
|
28
32
|
|
|
@@ -53,6 +57,14 @@ export function oauthProvidersRouteDefinitions(): RouteDefinition[] {
|
|
|
53
57
|
);
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
if (!isProviderVisible(row, loadConfig())) {
|
|
61
|
+
return httpError(
|
|
62
|
+
"NOT_FOUND",
|
|
63
|
+
`No OAuth provider registered for "${params.providerKey}"`,
|
|
64
|
+
404,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
return Response.json({ provider: serializeProviderSummary(row) });
|
|
57
69
|
},
|
|
58
70
|
},
|
|
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|
|
11
11
|
import {
|
|
12
12
|
getUsageDayBuckets,
|
|
13
13
|
getUsageGroupBreakdown,
|
|
14
|
+
getUsageHourBuckets,
|
|
14
15
|
getUsageTotals,
|
|
15
16
|
} from "../../memory/llm-usage-store.js";
|
|
16
17
|
import { httpError } from "../http-errors.js";
|
|
@@ -104,14 +105,30 @@ export function usageRouteDefinitions(): RouteDefinition[] {
|
|
|
104
105
|
schema: { type: "integer" },
|
|
105
106
|
description: "End epoch millis (required)",
|
|
106
107
|
},
|
|
108
|
+
{
|
|
109
|
+
name: "granularity",
|
|
110
|
+
schema: { type: "string", enum: ["daily", "hourly"] },
|
|
111
|
+
description: 'Bucket granularity: "daily" (default) or "hourly"',
|
|
112
|
+
},
|
|
107
113
|
],
|
|
108
114
|
responseBody: z.object({
|
|
109
|
-
buckets: z.array(z.unknown()).describe("
|
|
115
|
+
buckets: z.array(z.unknown()).describe("Usage bucket objects"),
|
|
110
116
|
}),
|
|
111
117
|
handler: ({ url }) => {
|
|
112
118
|
const range = parseTimeRange(url);
|
|
113
119
|
if (range instanceof Response) return range;
|
|
114
|
-
const
|
|
120
|
+
const granularity = url.searchParams.get("granularity") ?? "daily";
|
|
121
|
+
if (granularity !== "daily" && granularity !== "hourly") {
|
|
122
|
+
return httpError(
|
|
123
|
+
"BAD_REQUEST",
|
|
124
|
+
`Invalid "granularity" value: "${granularity}". Must be one of: daily, hourly`,
|
|
125
|
+
400,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const buckets =
|
|
129
|
+
granularity === "hourly"
|
|
130
|
+
? getUsageHourBuckets(range)
|
|
131
|
+
: getUsageDayBuckets(range);
|
|
115
132
|
return Response.json({ buckets });
|
|
116
133
|
},
|
|
117
134
|
},
|
|
@@ -5,24 +5,8 @@
|
|
|
5
5
|
* item falls back to the task-level required tools instead of silently
|
|
6
6
|
* skipping permission checks.
|
|
7
7
|
*/
|
|
8
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
8
|
import { afterAll, describe, expect, mock, test } from "bun:test";
|
|
12
9
|
|
|
13
|
-
const testDir = mkdtempSync(join(tmpdir(), "work-items-routes-test-"));
|
|
14
|
-
|
|
15
|
-
mock.module("../../util/platform.js", () => ({
|
|
16
|
-
getDataDir: () => testDir,
|
|
17
|
-
isMacOS: () => process.platform === "darwin",
|
|
18
|
-
isLinux: () => process.platform === "linux",
|
|
19
|
-
isWindows: () => process.platform === "win32",
|
|
20
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
21
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
22
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
23
|
-
ensureDataDir: () => {},
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
10
|
mock.module("../../util/logger.js", () => ({
|
|
27
11
|
getLogger: () =>
|
|
28
12
|
new Proxy({} as Record<string, unknown>, {
|
|
@@ -48,11 +32,6 @@ initializeDb();
|
|
|
48
32
|
|
|
49
33
|
afterAll(() => {
|
|
50
34
|
resetDb();
|
|
51
|
-
try {
|
|
52
|
-
rmSync(testDir, { recursive: true });
|
|
53
|
-
} catch {
|
|
54
|
-
/* best effort */
|
|
55
|
-
}
|
|
56
35
|
});
|
|
57
36
|
|
|
58
37
|
describe("empty required_tools snapshot bypass", () => {
|
|
@@ -4,37 +4,17 @@
|
|
|
4
4
|
* Covers path resolution (traversal prevention), MIME type detection,
|
|
5
5
|
* directory listing, file metadata, and raw content serving with range support.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
existsSync,
|
|
9
|
-
mkdirSync,
|
|
10
|
-
mkdtempSync,
|
|
11
|
-
readFileSync,
|
|
12
|
-
realpathSync,
|
|
13
|
-
rmSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
} from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
8
|
import { join } from "node:path";
|
|
18
|
-
import {
|
|
9
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
19
10
|
|
|
20
11
|
// ---------------------------------------------------------------------------
|
|
21
12
|
// Create a temp workspace directory for isolation
|
|
22
13
|
// ---------------------------------------------------------------------------
|
|
23
14
|
|
|
24
|
-
const testWorkspaceDir =
|
|
25
|
-
mkdtempSync(join(tmpdir(), "workspace-routes-test-")),
|
|
26
|
-
);
|
|
15
|
+
const testWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
27
16
|
|
|
28
17
|
// Mock platform module so getWorkspaceDir returns our temp dir
|
|
29
|
-
mock.module("../../util/platform.js", () => ({
|
|
30
|
-
getWorkspaceDir: () => testWorkspaceDir,
|
|
31
|
-
getRootDir: () => testWorkspaceDir,
|
|
32
|
-
getDataDir: () => testWorkspaceDir,
|
|
33
|
-
isMacOS: () => process.platform === "darwin",
|
|
34
|
-
isLinux: () => process.platform === "linux",
|
|
35
|
-
isWindows: () => process.platform === "win32",
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
18
|
import { workspaceRouteDefinitions } from "./workspace-routes.js";
|
|
39
19
|
import { isTextMimeType, resolveWorkspacePath } from "./workspace-utils.js";
|
|
40
20
|
|
|
@@ -65,10 +45,6 @@ beforeAll(() => {
|
|
|
65
45
|
writeFileSync(binaryFile, pngSignature);
|
|
66
46
|
});
|
|
67
47
|
|
|
68
|
-
afterAll(() => {
|
|
69
|
-
rmSync(testWorkspaceDir, { recursive: true, force: true });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
48
|
// ---------------------------------------------------------------------------
|
|
73
49
|
// Helpers
|
|
74
50
|
// ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User-defined allowlist for suppressing secret scanner false positives.
|
|
3
3
|
*
|
|
4
|
-
* Reads `~/.vellum/secret-allowlist.json` (if present) and provides
|
|
4
|
+
* Reads `~/.vellum/workspace/data/secret-allowlist.json` (if present) and provides
|
|
5
5
|
* `isAllowlisted(value)` to check whether a matched value should be
|
|
6
6
|
* suppressed.
|
|
7
7
|
*
|
|
@@ -18,7 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
|
|
19
19
|
import { pathExists } from "../util/fs.js";
|
|
20
20
|
import { getLogger } from "../util/logger.js";
|
|
21
|
-
import {
|
|
21
|
+
import { getDataDir } from "../util/platform.js";
|
|
22
22
|
|
|
23
23
|
const log = getLogger("secret-allowlist");
|
|
24
24
|
|
|
@@ -45,7 +45,7 @@ let allowedRegexes: RegExp[] = [];
|
|
|
45
45
|
export function loadAllowlist(): void {
|
|
46
46
|
if (loaded || fileChecked) return;
|
|
47
47
|
|
|
48
|
-
const filePath = join(
|
|
48
|
+
const filePath = join(getDataDir(), "secret-allowlist.json");
|
|
49
49
|
if (!pathExists(filePath)) {
|
|
50
50
|
fileChecked = true;
|
|
51
51
|
return;
|
|
@@ -169,7 +169,7 @@ function validateAllowlist(
|
|
|
169
169
|
* Returns validation errors, or null if the file doesn't exist.
|
|
170
170
|
*/
|
|
171
171
|
export function validateAllowlistFile(): AllowlistValidationError[] | null {
|
|
172
|
-
const filePath = join(
|
|
172
|
+
const filePath = join(getDataDir(), "secret-allowlist.json");
|
|
173
173
|
if (!pathExists(filePath)) return null;
|
|
174
174
|
|
|
175
175
|
const raw = readFileSync(filePath, "utf-8");
|