@vellumai/assistant 0.10.9-staging.1 → 0.10.9-staging.2
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/AGENTS.md +4 -0
- package/knip.json +1 -4
- package/openapi.yaml +28 -18
- package/package.json +1 -1
- package/src/__tests__/always-loaded-tools-guard.test.ts +4 -6
- package/src/__tests__/app-routes-csp.test.ts +13 -4
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +4 -8
- package/src/__tests__/disk-pressure-tools.test.ts +4 -4
- package/src/__tests__/file-list-tool.test.ts +22 -12
- package/src/__tests__/list-all-apps.test.ts +204 -0
- package/src/__tests__/messaging-send-tool.test.ts +67 -0
- package/src/__tests__/mtime-cache.test.ts +42 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +3 -2
- package/src/__tests__/outlook-messaging-provider.test.ts +71 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +163 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +47 -0
- package/src/__tests__/subagent-notify-parent.test.ts +4 -3
- package/src/__tests__/subagent-tool-filtering.test.ts +21 -21
- package/src/__tests__/subagent-tool-gate-mode.test.ts +6 -6
- package/src/apps/app-store.ts +302 -2
- package/src/cli/AGENTS.md +17 -0
- package/src/cli/commands/__tests__/gateway.test.ts +55 -0
- package/src/cli/commands/apps.help.ts +17 -11
- package/src/cli/commands/apps.ts +5 -15
- package/src/cli/commands/gateway.help.ts +30 -0
- package/src/cli/commands/gateway.ts +40 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -90
- package/src/cli/commands/platform/index.help.ts +3 -2
- package/src/cli/commands/platform/index.ts +0 -9
- package/src/cli/commands/routes.help.ts +13 -7
- package/src/cli/commands/routes.ts +5 -3
- package/src/cli/commands/status.help.ts +24 -0
- package/src/cli/commands/status.ts +29 -0
- package/src/cli/lib/__tests__/global-json-option.test.ts +97 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +1 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +31 -0
- package/src/cli/lib/bundled-marketplace.json +187 -173
- package/src/cli/lib/global-json-option.ts +41 -0
- package/src/cli/lib/plugin-fingerprint.ts +18 -7
- package/src/cli/program.ts +5 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -19
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/feature-flag-registry.json +1 -9
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +8 -5
- package/src/daemon/conversation-tool-setup.ts +5 -90
- package/src/daemon/conversation.ts +2 -2
- package/src/messaging/provider-types.ts +9 -0
- package/src/messaging/providers/outlook/adapter.ts +17 -2
- package/src/messaging/providers/outlook/client.ts +11 -2
- package/src/messaging/providers/outlook/types.ts +9 -0
- package/src/monitoring/__tests__/plugin-source-watch.test.ts +37 -0
- package/src/monitoring/plugin-source-watch.ts +53 -0
- package/src/oauth/seed-providers.ts +6 -0
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +3 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +11 -0
- package/src/plugin-api/conversation-turn.ts +234 -0
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/__tests__/source-fingerprint.test.ts +41 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +48 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +46 -5
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +2 -2
- package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +81 -2
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +80 -33
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +40 -1
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +28 -2
- package/src/plugins/mtime-cache.ts +26 -13
- package/src/plugins/plugin-tree-walk.ts +30 -0
- package/src/plugins/source-fingerprint.ts +9 -2
- package/src/plugins/surface-import.ts +28 -0
- package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +1 -1
- package/src/providers/speech-to-text/__tests__/vellum-speech-relay-connection.test.ts +4 -1
- package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +5 -0
- package/src/runtime/routes/__tests__/gateway-status-routes.test.ts +84 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +47 -3
- package/src/runtime/routes/__tests__/user-routes-cli.test.ts +154 -0
- package/src/runtime/routes/app-management-routes.ts +96 -32
- package/src/runtime/routes/app-routes.ts +33 -17
- package/src/runtime/routes/gateway-status-routes.ts +69 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +4 -14
- package/src/runtime/routes/plugins-routes.ts +58 -1
- package/src/runtime/routes/settings-routes.ts +8 -6
- package/src/runtime/routes/user-route-dispatcher.ts +6 -86
- package/src/runtime/routes/user-route-resolution.ts +141 -0
- package/src/runtime/routes/user-routes-cli.ts +56 -35
- package/src/telemetry/AGENTS.md +15 -0
- package/src/telemetry/__tests__/telemetry-event-fixtures.ts +258 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire-validation.test.ts +157 -0
- package/src/telemetry/telemetry-wire-validation.ts +125 -0
- package/src/telemetry/telemetry-wire.generated.ts +60 -9
- package/src/telemetry/types.test.ts +38 -0
- package/src/telemetry/types.ts +183 -91
- package/src/telemetry/usage-telemetry-reporter.ts +5 -0
- package/src/telemetry/watchdog-direct-emit.ts +6 -0
- package/src/tools/skills/scaffold-managed.ts +43 -11
package/AGENTS.md
CHANGED
|
@@ -73,6 +73,10 @@ Some routes are IPC-only (defined in `src/ipc/routes/`, not in the shared array)
|
|
|
73
73
|
|
|
74
74
|
The module-level dependency-injection pattern (`registerFooDeps()`) used by some IPC routes is a known antipattern. New IPC-only routes should avoid it.
|
|
75
75
|
|
|
76
|
+
## Telemetry wire contract
|
|
77
|
+
|
|
78
|
+
Telemetry event types are defined by a platform-generated wire contract (`src/telemetry/telemetry-wire.generated.ts`) that `src/telemetry/types.ts` layers over, with pre-flush validation against it. Adding a new event type starts platform-side, not here. The mechanics, the drift guards, and the cross-repo ordering are documented next to the code they govern: see [`src/telemetry/AGENTS.md`](src/telemetry/AGENTS.md).
|
|
79
|
+
|
|
76
80
|
## Code comments
|
|
77
81
|
|
|
78
82
|
When writing or updating comments, **do not reference code that has been removed.** Comments should describe the current state of the codebase, not narrate its history. Avoid phrases like "no longer does X", "previously used Y", or "was removed in PR Z" — future readers should not need to understand past implementations to understand the current code.
|
package/knip.json
CHANGED
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
"src/config/bundled-skills/**/tools/**/*.ts!"
|
|
10
10
|
],
|
|
11
11
|
"project": ["src/**/*.ts!", "src/**/*.tsx!", "scripts/**/*.ts"],
|
|
12
|
-
"ignore": [
|
|
13
|
-
"src/config/preloaded-apps/**",
|
|
14
|
-
"src/telemetry/telemetry-wire.generated.ts"
|
|
15
|
-
],
|
|
12
|
+
"ignore": ["src/config/preloaded-apps/**"],
|
|
16
13
|
"ignoreDependencies": [
|
|
17
14
|
"@microsoft/api-extractor",
|
|
18
15
|
"@vellumai/ces-client",
|
package/openapi.yaml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
openapi: 3.1.0
|
|
4
4
|
info:
|
|
5
5
|
title: Vellum Assistant API
|
|
6
|
-
version: 0.10.
|
|
6
|
+
version: 0.10.8
|
|
7
7
|
description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
|
|
8
8
|
servers:
|
|
9
9
|
- url: http://127.0.0.1:7821
|
|
@@ -606,6 +606,8 @@ paths:
|
|
|
606
606
|
type: string
|
|
607
607
|
contentId:
|
|
608
608
|
type: string
|
|
609
|
+
origin:
|
|
610
|
+
type: string
|
|
609
611
|
required:
|
|
610
612
|
- id
|
|
611
613
|
- name
|
|
@@ -613,6 +615,7 @@ paths:
|
|
|
613
615
|
- updatedAt
|
|
614
616
|
- version
|
|
615
617
|
- contentId
|
|
618
|
+
- origin
|
|
616
619
|
additionalProperties: false
|
|
617
620
|
required:
|
|
618
621
|
- apps
|
|
@@ -11554,6 +11557,27 @@ paths:
|
|
|
11554
11557
|
- lines
|
|
11555
11558
|
- truncated
|
|
11556
11559
|
additionalProperties: false
|
|
11560
|
+
/v1/gateway/status:
|
|
11561
|
+
get:
|
|
11562
|
+
operationId: gateway_status_get
|
|
11563
|
+
summary: Get gateway status
|
|
11564
|
+
description:
|
|
11565
|
+
Reports the gateway's public tunnel status. `tunnel` holds the active public URL when a tunnel is connected
|
|
11566
|
+
and is omitted otherwise. Errors with 503 when the gateway is not running. The tunnel only matters for routing
|
|
11567
|
+
inbound Twilio webhooks and live voice/audio WebSockets.
|
|
11568
|
+
tags:
|
|
11569
|
+
- gateway
|
|
11570
|
+
responses:
|
|
11571
|
+
"200":
|
|
11572
|
+
description: Successful response
|
|
11573
|
+
content:
|
|
11574
|
+
application/json:
|
|
11575
|
+
schema:
|
|
11576
|
+
type: object
|
|
11577
|
+
properties:
|
|
11578
|
+
tunnel:
|
|
11579
|
+
type: string
|
|
11580
|
+
additionalProperties: false
|
|
11557
11581
|
/v1/groups:
|
|
11558
11582
|
get:
|
|
11559
11583
|
operationId: groups_get
|
|
@@ -21925,7 +21949,9 @@ paths:
|
|
|
21925
21949
|
get:
|
|
21926
21950
|
operationId: platform_status_get
|
|
21927
21951
|
summary: Get platform deployment context and connection status
|
|
21928
|
-
description:
|
|
21952
|
+
description:
|
|
21953
|
+
Aggregates platform context, credentials, assistant ID, and webhook secret. Velay tunnel status is reported
|
|
21954
|
+
separately by gateway_status.
|
|
21929
21955
|
tags:
|
|
21930
21956
|
- platform
|
|
21931
21957
|
responses:
|
|
@@ -21960,21 +21986,6 @@ paths:
|
|
|
21960
21986
|
anyOf:
|
|
21961
21987
|
- type: string
|
|
21962
21988
|
- type: "null"
|
|
21963
|
-
velayTunnel:
|
|
21964
|
-
anyOf:
|
|
21965
|
-
- type: object
|
|
21966
|
-
properties:
|
|
21967
|
-
connected:
|
|
21968
|
-
type: boolean
|
|
21969
|
-
publicUrl:
|
|
21970
|
-
anyOf:
|
|
21971
|
-
- type: string
|
|
21972
|
-
- type: "null"
|
|
21973
|
-
required:
|
|
21974
|
-
- connected
|
|
21975
|
-
- publicUrl
|
|
21976
|
-
additionalProperties: false
|
|
21977
|
-
- type: "null"
|
|
21978
21989
|
required:
|
|
21979
21990
|
- isPlatform
|
|
21980
21991
|
- baseUrl
|
|
@@ -21985,7 +21996,6 @@ paths:
|
|
|
21985
21996
|
- available
|
|
21986
21997
|
- organizationId
|
|
21987
21998
|
- userId
|
|
21988
|
-
- velayTunnel
|
|
21989
21999
|
additionalProperties: false
|
|
21990
22000
|
/v1/playground/seed-conversation:
|
|
21991
22001
|
post:
|
package/package.json
CHANGED
|
@@ -13,10 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import { afterAll, describe, expect, test } from "bun:test";
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
type SkillProjectionContext,
|
|
19
|
-
} from "../daemon/conversation-tool-setup.js";
|
|
16
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
17
|
+
import { isToolActiveForContext } from "../daemon/conversation-tool-setup.js";
|
|
20
18
|
import {
|
|
21
19
|
__resetRegistryForTesting,
|
|
22
20
|
getAllToolDefinitions,
|
|
@@ -33,13 +31,13 @@ describe("always-loaded tool count", () => {
|
|
|
33
31
|
const allDefs = getAllToolDefinitions();
|
|
34
32
|
|
|
35
33
|
// Minimal context: no client, no capabilities
|
|
36
|
-
const minimalContext
|
|
34
|
+
const minimalContext = {
|
|
37
35
|
skillProjectionState: new Map(),
|
|
38
36
|
skillProjectionCache: {},
|
|
39
37
|
toolsDisabledDepth: 0,
|
|
40
38
|
hasNoClient: true,
|
|
41
39
|
channelCapabilities: undefined,
|
|
42
|
-
};
|
|
40
|
+
} as unknown as Conversation;
|
|
43
41
|
|
|
44
42
|
const activeTools = allDefs.filter((def) =>
|
|
45
43
|
isToolActiveForContext(def.name, minimalContext),
|
|
@@ -27,10 +27,19 @@ const apps = new Map<string, typeof legacyApp | typeof multifileApp>([
|
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
29
|
mock.module("../apps/app-store.js", () => ({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
resolveAppSource: (id: string) => {
|
|
31
|
+
const app = apps.get(id);
|
|
32
|
+
if (!app) return null;
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
name: app.name,
|
|
36
|
+
dirName: id,
|
|
37
|
+
sourceDir: `/fake/apps/${id}`,
|
|
38
|
+
origin: { kind: "workspace" as const },
|
|
39
|
+
formatVersion: (app as { formatVersion?: number }).formatVersion ?? 1,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
readAppFileBytesFromDir: () => Buffer.from(""),
|
|
34
43
|
}));
|
|
35
44
|
|
|
36
45
|
// Mock shared-app-links-store (imported by app-routes but unused here)
|
|
@@ -28,10 +28,8 @@ mock.module("../daemon/conversation-skill-tools.js", () => ({
|
|
|
28
28
|
// Import after mocks
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
type SkillProjectionContext,
|
|
34
|
-
} from "../daemon/conversation-tool-setup.js";
|
|
31
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
32
|
+
import { createResolveToolsCallback } from "../daemon/conversation-tool-setup.js";
|
|
35
33
|
|
|
36
34
|
// ---------------------------------------------------------------------------
|
|
37
35
|
// Helpers
|
|
@@ -41,15 +39,13 @@ function makeToolDef(name: string): ToolDefinition {
|
|
|
41
39
|
return { name, description: `${name} tool`, input_schema: {} };
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
function makeCtx(
|
|
45
|
-
overrides: Partial<SkillProjectionContext> = {},
|
|
46
|
-
): SkillProjectionContext {
|
|
42
|
+
function makeCtx(overrides: Partial<Conversation> = {}): Conversation {
|
|
47
43
|
return {
|
|
48
44
|
skillProjectionState: new Map(),
|
|
49
45
|
skillProjectionCache: {} as SkillProjectionCache,
|
|
50
46
|
toolsDisabledDepth: 0,
|
|
51
47
|
...overrides,
|
|
52
|
-
};
|
|
48
|
+
} as unknown as Conversation;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
const EMPTY_HISTORY: Message[] = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
3
4
|
import type { SkillProjectionCache } from "../daemon/conversation-skill-tools.js";
|
|
4
|
-
import type { SkillProjectionContext } from "../daemon/conversation-tool-setup.js";
|
|
5
5
|
import type { Message, ToolDefinition } from "../providers/types.js";
|
|
6
6
|
import type { DiskUsageInfo } from "../util/disk-usage.js";
|
|
7
7
|
import * as realDiskUsage from "../util/disk-usage.js";
|
|
@@ -74,14 +74,14 @@ function makeToolDef(name: string): ToolDefinition {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function makeProjectionCtx(
|
|
77
|
-
overrides: Partial<
|
|
78
|
-
):
|
|
77
|
+
overrides: Partial<Conversation> = {},
|
|
78
|
+
): Conversation {
|
|
79
79
|
return {
|
|
80
80
|
skillProjectionState: new Map(),
|
|
81
81
|
skillProjectionCache: {} as SkillProjectionCache,
|
|
82
82
|
toolsDisabledDepth: 0,
|
|
83
83
|
...overrides,
|
|
84
|
-
};
|
|
84
|
+
} as unknown as Conversation;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function setDiskUsage(usedMb: number, totalMb = 100): void {
|
|
@@ -9,9 +9,9 @@ import { tmpdir } from "node:os";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
11
11
|
|
|
12
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
12
13
|
import {
|
|
13
14
|
isToolActiveForContext,
|
|
14
|
-
type SkillProjectionContext,
|
|
15
15
|
SUBAGENT_ONLY_TOOL_NAMES,
|
|
16
16
|
} from "../daemon/conversation-tool-setup.js";
|
|
17
17
|
import { fileListTool } from "../tools/filesystem/list.js";
|
|
@@ -60,7 +60,9 @@ describe("FileSystemOps.listDirSafe", () => {
|
|
|
60
60
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
61
61
|
const result = ops.listDirSafe({ path: dir });
|
|
62
62
|
expect(result.ok).toBe(true);
|
|
63
|
-
if (!result.ok)
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
64
66
|
|
|
65
67
|
const lines = result.value.listing.split("\n");
|
|
66
68
|
// Directories first with trailing /
|
|
@@ -81,7 +83,9 @@ describe("FileSystemOps.listDirSafe", () => {
|
|
|
81
83
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
82
84
|
const result = ops.listDirSafe({ path: dir, glob: "*.md" });
|
|
83
85
|
expect(result.ok).toBe(true);
|
|
84
|
-
if (!result.ok)
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
85
89
|
|
|
86
90
|
const lines = result.value.listing.split("\n");
|
|
87
91
|
expect(lines.length).toBe(2);
|
|
@@ -96,7 +100,9 @@ describe("FileSystemOps.listDirSafe", () => {
|
|
|
96
100
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
97
101
|
const result = ops.listDirSafe({ path: join(dir, "regular-file.txt") });
|
|
98
102
|
expect(result.ok).toBe(false);
|
|
99
|
-
if (result.ok)
|
|
103
|
+
if (result.ok) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
100
106
|
expect(result.error.code).toBe("NOT_A_DIRECTORY");
|
|
101
107
|
});
|
|
102
108
|
|
|
@@ -106,7 +112,9 @@ describe("FileSystemOps.listDirSafe", () => {
|
|
|
106
112
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
107
113
|
const result = ops.listDirSafe({ path: join(dir, "nonexistent") });
|
|
108
114
|
expect(result.ok).toBe(false);
|
|
109
|
-
if (result.ok)
|
|
115
|
+
if (result.ok) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
110
118
|
expect(result.error.code).toBe("NOT_FOUND");
|
|
111
119
|
});
|
|
112
120
|
|
|
@@ -116,7 +124,9 @@ describe("FileSystemOps.listDirSafe", () => {
|
|
|
116
124
|
const ops = new FileSystemOps(sandboxPolicyFor(dir));
|
|
117
125
|
const result = ops.listDirSafe({ path: "../../../etc" });
|
|
118
126
|
expect(result.ok).toBe(false);
|
|
119
|
-
if (result.ok)
|
|
127
|
+
if (result.ok) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
120
130
|
expect(result.error.code).toBe("PATH_OUT_OF_BOUNDS");
|
|
121
131
|
});
|
|
122
132
|
});
|
|
@@ -186,31 +196,31 @@ describe("file_list subagent visibility", () => {
|
|
|
186
196
|
});
|
|
187
197
|
|
|
188
198
|
test("isToolActiveForContext hides file_list when isSubagent is false", () => {
|
|
189
|
-
const ctx
|
|
199
|
+
const ctx = {
|
|
190
200
|
skillProjectionState: new Map(),
|
|
191
201
|
skillProjectionCache: {},
|
|
192
202
|
toolsDisabledDepth: 0,
|
|
193
|
-
};
|
|
203
|
+
} as unknown as Conversation;
|
|
194
204
|
expect(isToolActiveForContext("file_list", ctx)).toBe(false);
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
test("isToolActiveForContext hides file_list when isSubagent is undefined", () => {
|
|
198
|
-
const ctx
|
|
208
|
+
const ctx = {
|
|
199
209
|
skillProjectionState: new Map(),
|
|
200
210
|
skillProjectionCache: {},
|
|
201
211
|
toolsDisabledDepth: 0,
|
|
202
212
|
isSubagent: undefined,
|
|
203
|
-
};
|
|
213
|
+
} as unknown as Conversation;
|
|
204
214
|
expect(isToolActiveForContext("file_list", ctx)).toBe(false);
|
|
205
215
|
});
|
|
206
216
|
|
|
207
217
|
test("isToolActiveForContext shows file_list when isSubagent is true", () => {
|
|
208
|
-
const ctx
|
|
218
|
+
const ctx = {
|
|
209
219
|
skillProjectionState: new Map(),
|
|
210
220
|
skillProjectionCache: {},
|
|
211
221
|
toolsDisabledDepth: 0,
|
|
212
222
|
isSubagent: true,
|
|
213
|
-
};
|
|
223
|
+
} as unknown as Conversation;
|
|
214
224
|
expect(isToolActiveForContext("file_list", ctx)).toBe(true);
|
|
215
225
|
});
|
|
216
226
|
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createApp,
|
|
8
|
+
listAllApps,
|
|
9
|
+
listPluginApps,
|
|
10
|
+
resolveAppSource,
|
|
11
|
+
} from "../apps/app-store.js";
|
|
12
|
+
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
13
|
+
|
|
14
|
+
let workspaceDir: string;
|
|
15
|
+
|
|
16
|
+
function freshWorkspace(): string {
|
|
17
|
+
return join(
|
|
18
|
+
tmpdir(),
|
|
19
|
+
`vellum-list-all-apps-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Materialize an installed plugin under `<workspace>/plugins/<name>/` with a
|
|
25
|
+
* package.json manifest. Returns the plugin directory path.
|
|
26
|
+
*/
|
|
27
|
+
function installPlugin(
|
|
28
|
+
name: string,
|
|
29
|
+
opts: { disabled?: boolean } = {},
|
|
30
|
+
): string {
|
|
31
|
+
const pluginDir = join(getWorkspacePluginsDir(), name);
|
|
32
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(pluginDir, "package.json"),
|
|
35
|
+
JSON.stringify({ name, version: "1.0.0" }),
|
|
36
|
+
);
|
|
37
|
+
if (opts.disabled) {
|
|
38
|
+
writeFileSync(join(pluginDir, ".disabled"), "");
|
|
39
|
+
}
|
|
40
|
+
return pluginDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bundle an app inside a plugin as a directory under `<pluginDir>/apps/<app>/`.
|
|
45
|
+
*/
|
|
46
|
+
function bundleApp(pluginDir: string, app: string): void {
|
|
47
|
+
const appDir = join(pluginDir, "apps", app);
|
|
48
|
+
mkdirSync(appDir, { recursive: true });
|
|
49
|
+
writeFileSync(join(appDir, "index.html"), "<h1>Plugin app</h1>");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
workspaceDir = freshWorkspace();
|
|
54
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("listAllApps", () => {
|
|
62
|
+
test("returns nothing when no apps exist", () => {
|
|
63
|
+
expect(listAllApps()).toEqual([]);
|
|
64
|
+
expect(listPluginApps()).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("tags workspace apps with the workspace origin", () => {
|
|
68
|
+
const created = createApp({
|
|
69
|
+
name: "Budget",
|
|
70
|
+
schemaJson: "{}",
|
|
71
|
+
htmlDefinition: "<h1>Budget</h1>",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const all = listAllApps();
|
|
75
|
+
expect(all).toHaveLength(1);
|
|
76
|
+
expect(all[0].origin).toEqual({ kind: "workspace" });
|
|
77
|
+
expect(all[0].id).toBe(created.id);
|
|
78
|
+
expect(all[0].name).toBe("Budget");
|
|
79
|
+
// Workspace source resolves through the dirName-based resolver.
|
|
80
|
+
expect(all[0].sourcePath).toContain(join("data", "apps"));
|
|
81
|
+
expect(all[0].sourcePath).toContain(created.dirName ?? created.id);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("discovers plugin apps by directory, tagged plugin:<name>", () => {
|
|
85
|
+
createApp({
|
|
86
|
+
name: "Budget",
|
|
87
|
+
schemaJson: "{}",
|
|
88
|
+
htmlDefinition: "<h1>Budget</h1>",
|
|
89
|
+
});
|
|
90
|
+
const pluginDir = installPlugin("acme");
|
|
91
|
+
bundleApp(pluginDir, "acme-dashboard");
|
|
92
|
+
|
|
93
|
+
const plugin = listPluginApps();
|
|
94
|
+
expect(plugin).toHaveLength(1);
|
|
95
|
+
expect(plugin[0].origin).toEqual({ kind: "plugin", pluginName: "acme" });
|
|
96
|
+
expect(plugin[0].id).toBe("plugins~acme~acme-dashboard");
|
|
97
|
+
expect(plugin[0].name).toBe("acme-dashboard");
|
|
98
|
+
expect(plugin[0].sourcePath).toBe(
|
|
99
|
+
join(pluginDir, "apps", "acme-dashboard"),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Workspace apps come first, then plugin apps.
|
|
103
|
+
const all = listAllApps();
|
|
104
|
+
expect(all.map((a) => a.origin.kind)).toEqual(["workspace", "plugin"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("ignores directories under plugins/ without a package.json manifest", () => {
|
|
108
|
+
// A stray directory that is not an installed plugin, even with an apps/ dir.
|
|
109
|
+
const strayDir = join(getWorkspacePluginsDir(), "not-a-plugin");
|
|
110
|
+
mkdirSync(join(strayDir, "apps", "ghost"), { recursive: true });
|
|
111
|
+
|
|
112
|
+
expect(listPluginApps()).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("includes apps from a symlinked plugin directory", () => {
|
|
116
|
+
// Real plugin lives outside plugins/; plugins/<name> is a symlink to it.
|
|
117
|
+
const realPluginDir = join(workspaceDir, "external-acme");
|
|
118
|
+
mkdirSync(realPluginDir, { recursive: true });
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(realPluginDir, "package.json"),
|
|
121
|
+
JSON.stringify({ name: "acme", version: "1.0.0" }),
|
|
122
|
+
);
|
|
123
|
+
bundleApp(realPluginDir, "acme-dashboard");
|
|
124
|
+
|
|
125
|
+
mkdirSync(getWorkspacePluginsDir(), { recursive: true });
|
|
126
|
+
symlinkSync(realPluginDir, join(getWorkspacePluginsDir(), "acme"));
|
|
127
|
+
|
|
128
|
+
const plugin = listPluginApps();
|
|
129
|
+
expect(plugin).toHaveLength(1);
|
|
130
|
+
expect(plugin[0].name).toBe("acme-dashboard");
|
|
131
|
+
expect(plugin[0].origin).toEqual({ kind: "plugin", pluginName: "acme" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("excludes apps from disabled plugins", () => {
|
|
135
|
+
const pluginDir = installPlugin("acme", { disabled: true });
|
|
136
|
+
bundleApp(pluginDir, "acme-dashboard");
|
|
137
|
+
|
|
138
|
+
expect(listPluginApps()).toEqual([]);
|
|
139
|
+
expect(listAllApps()).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("resolveAppSource", () => {
|
|
144
|
+
test("resolves a workspace app by its UUID", () => {
|
|
145
|
+
const created = createApp({
|
|
146
|
+
name: "Budget",
|
|
147
|
+
schemaJson: "{}",
|
|
148
|
+
htmlDefinition: "<h1>Budget</h1>",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const source = resolveAppSource(created.id);
|
|
152
|
+
expect(source).not.toBeNull();
|
|
153
|
+
expect(source!.origin).toEqual({ kind: "workspace" });
|
|
154
|
+
expect(source!.name).toBe("Budget");
|
|
155
|
+
expect(source!.sourceDir).toContain(join("data", "apps"));
|
|
156
|
+
// Single-file app (index.html at root).
|
|
157
|
+
expect(source!.formatVersion).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("resolves a plugin app by its plugins~<name>~<app> id", () => {
|
|
161
|
+
const pluginDir = installPlugin("acme");
|
|
162
|
+
bundleApp(pluginDir, "acme-dashboard");
|
|
163
|
+
|
|
164
|
+
const source = resolveAppSource("plugins~acme~acme-dashboard");
|
|
165
|
+
expect(source).not.toBeNull();
|
|
166
|
+
expect(source!.origin).toEqual({ kind: "plugin", pluginName: "acme" });
|
|
167
|
+
expect(source!.name).toBe("acme-dashboard");
|
|
168
|
+
expect(source!.sourceDir).toBe(join(pluginDir, "apps", "acme-dashboard"));
|
|
169
|
+
// bundleApp writes index.html at the root → single-file.
|
|
170
|
+
expect(source!.formatVersion).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("returns null for an unknown workspace id", () => {
|
|
174
|
+
expect(resolveAppSource("11111111-1111-1111-1111-111111111111")).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("returns null for a plugin app whose plugin is disabled", () => {
|
|
178
|
+
const pluginDir = installPlugin("acme", { disabled: true });
|
|
179
|
+
bundleApp(pluginDir, "acme-dashboard");
|
|
180
|
+
|
|
181
|
+
expect(resolveAppSource("plugins~acme~acme-dashboard")).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("returns null for a plugin dir without a package.json manifest", () => {
|
|
185
|
+
// Stray directory shaped like a plugin app but not an installed plugin.
|
|
186
|
+
const strayApp = join(
|
|
187
|
+
getWorkspacePluginsDir(),
|
|
188
|
+
"not-a-plugin",
|
|
189
|
+
"apps",
|
|
190
|
+
"x",
|
|
191
|
+
);
|
|
192
|
+
mkdirSync(strayApp, { recursive: true });
|
|
193
|
+
|
|
194
|
+
expect(resolveAppSource("plugins~not-a-plugin~x")).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("returns null for traversal and malformed plugin ids", () => {
|
|
198
|
+
installPlugin("acme");
|
|
199
|
+
expect(resolveAppSource("plugins~..~secrets")).toBeNull(); // traversal name
|
|
200
|
+
expect(resolveAppSource("plugins~acme~..")).toBeNull(); // traversal app
|
|
201
|
+
expect(resolveAppSource("plugins~acme")).toBeNull(); // missing app segment
|
|
202
|
+
expect(resolveAppSource("plugins~acme~sub/dir")).toBeNull(); // slash in app
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
5
|
|
|
3
6
|
import type { MessagingProvider } from "../messaging/provider.js";
|
|
@@ -129,6 +132,7 @@ describe("messaging-send tool", () => {
|
|
|
129
132
|
subject: undefined,
|
|
130
133
|
inReplyTo: undefined,
|
|
131
134
|
threadId: undefined,
|
|
135
|
+
attachments: undefined,
|
|
132
136
|
assistantId: "ast-alpha",
|
|
133
137
|
},
|
|
134
138
|
);
|
|
@@ -159,11 +163,74 @@ describe("messaging-send tool", () => {
|
|
|
159
163
|
subject: undefined,
|
|
160
164
|
inReplyTo: undefined,
|
|
161
165
|
threadId: "thread-abc",
|
|
166
|
+
attachments: undefined,
|
|
162
167
|
assistantId: "ast-alpha",
|
|
163
168
|
},
|
|
164
169
|
);
|
|
165
170
|
});
|
|
166
171
|
|
|
172
|
+
test("rejects attachments on platforms that can't carry them", async () => {
|
|
173
|
+
const result = await run(
|
|
174
|
+
{
|
|
175
|
+
platform: "phone",
|
|
176
|
+
conversation_id: "+12025550142",
|
|
177
|
+
text: "with a file",
|
|
178
|
+
attachment_paths: ["/tmp/does-not-matter.pdf"],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
workingDir: "/tmp",
|
|
182
|
+
conversationId: "conv-1",
|
|
183
|
+
assistantId: "ast-alpha",
|
|
184
|
+
trustClass: "guardian" as const,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(result.isError).toBe(true);
|
|
189
|
+
expect(result.content).toContain("Gmail and Outlook");
|
|
190
|
+
expect(sendMessageMock).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("reads and forwards attachments to attachment-capable non-Gmail providers", async () => {
|
|
194
|
+
const dir = mkdtempSync(join(tmpdir(), "msg-send-att-"));
|
|
195
|
+
const filePath = join(dir, "report.pdf");
|
|
196
|
+
writeFileSync(filePath, "pdf-bytes");
|
|
197
|
+
provider.id = "outlook";
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const result = await run(
|
|
201
|
+
{
|
|
202
|
+
platform: "outlook",
|
|
203
|
+
conversation_id: "user@example.com",
|
|
204
|
+
text: "see attached",
|
|
205
|
+
subject: "Docs",
|
|
206
|
+
attachment_paths: [filePath],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
workingDir: "/tmp",
|
|
210
|
+
conversationId: "conv-1",
|
|
211
|
+
assistantId: "ast-alpha",
|
|
212
|
+
trustClass: "guardian" as const,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(result.isError).toBe(false);
|
|
217
|
+
const options = sendMessageMock.mock.calls[0][3] as {
|
|
218
|
+
attachments?: Array<{
|
|
219
|
+
filename: string;
|
|
220
|
+
mimeType: string;
|
|
221
|
+
data: Buffer;
|
|
222
|
+
}>;
|
|
223
|
+
};
|
|
224
|
+
expect(options.attachments).toHaveLength(1);
|
|
225
|
+
expect(options.attachments?.[0]?.filename).toBe("report.pdf");
|
|
226
|
+
expect(options.attachments?.[0]?.mimeType).toBe("application/pdf");
|
|
227
|
+
expect(options.attachments?.[0]?.data.toString()).toBe("pdf-bytes");
|
|
228
|
+
} finally {
|
|
229
|
+
provider.id = "phone";
|
|
230
|
+
rmSync(dir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
167
234
|
test("cross-posts outbound message to bound conversation", async () => {
|
|
168
235
|
getBindingByChannelChatMock.mockImplementation(() => ({
|
|
169
236
|
conversationId: "bound-conv-99",
|