@vellumai/assistant 0.5.12 → 0.5.13
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 +41 -9
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/openapi.yaml +1 -1
- package/package.json +1 -1
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/feature-flag-registry.json +1 -1
- package/src/credential-execution/client.ts +14 -2
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/lifecycle.ts +13 -8
- package/src/index.ts +0 -12
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/journal-memory.ts +8 -2
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +11 -0
- package/src/runtime/http-server.ts +7 -15
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/secret-routes.ts +9 -2
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/util/platform.ts +1 -91
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), "platform-status-test-"));
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Mock state
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
let mockGetSecureKeyViaDaemon: (
|
|
13
|
+
account: string,
|
|
14
|
+
) => Promise<string | undefined> = async () => undefined;
|
|
15
|
+
|
|
16
|
+
let mockResolvePlatformCallbackRegistrationContext: () => Promise<
|
|
17
|
+
Record<string, unknown>
|
|
18
|
+
> = async () => ({
|
|
19
|
+
containerized: false,
|
|
20
|
+
platformBaseUrl: "",
|
|
21
|
+
assistantId: "",
|
|
22
|
+
hasInternalApiKey: false,
|
|
23
|
+
hasAssistantApiKey: false,
|
|
24
|
+
authHeader: null,
|
|
25
|
+
enabled: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Mocks
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
mock.module("../../../../inbound/platform-callback-registration.js", () => ({
|
|
33
|
+
resolvePlatformCallbackRegistrationContext: () =>
|
|
34
|
+
mockResolvePlatformCallbackRegistrationContext(),
|
|
35
|
+
registerCallbackRoute: async () => "",
|
|
36
|
+
shouldUsePlatformCallbacks: () => false,
|
|
37
|
+
resolveCallbackUrl: async () => "",
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
mock.module("../../../lib/daemon-credential-client.js", () => ({
|
|
41
|
+
getSecureKeyViaDaemon: (account: string) =>
|
|
42
|
+
mockGetSecureKeyViaDaemon(account),
|
|
43
|
+
deleteSecureKeyViaDaemon: async () => "not-found" as const,
|
|
44
|
+
setSecureKeyViaDaemon: async () => false,
|
|
45
|
+
getProviderKeyViaDaemon: async () => undefined,
|
|
46
|
+
getSecureKeyResultViaDaemon: async () => ({
|
|
47
|
+
value: undefined,
|
|
48
|
+
unreachable: false,
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
53
|
+
getLogger: () => ({
|
|
54
|
+
info: () => {},
|
|
55
|
+
warn: () => {},
|
|
56
|
+
error: () => {},
|
|
57
|
+
debug: () => {},
|
|
58
|
+
}),
|
|
59
|
+
getCliLogger: () => ({
|
|
60
|
+
info: () => {},
|
|
61
|
+
warn: () => {},
|
|
62
|
+
error: () => {},
|
|
63
|
+
debug: () => {},
|
|
64
|
+
}),
|
|
65
|
+
initLogger: () => {},
|
|
66
|
+
truncateForLog: (value: string, maxLen = 500) =>
|
|
67
|
+
value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
|
|
68
|
+
pruneOldLogFiles: () => 0,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
mock.module("../../../../util/platform.js", () => ({
|
|
72
|
+
getRootDir: () => testDir,
|
|
73
|
+
getDataDir: () => join(testDir, "data"),
|
|
74
|
+
getWorkspaceSkillsDir: () => join(testDir, "skills"),
|
|
75
|
+
getWorkspaceDir: () => join(testDir, "workspace"),
|
|
76
|
+
getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
|
|
77
|
+
getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
|
|
78
|
+
getHooksDir: () => join(testDir, "hooks"),
|
|
79
|
+
getSignalsDir: () => join(testDir, "signals"),
|
|
80
|
+
getConversationsDir: () => join(testDir, "conversations"),
|
|
81
|
+
getEmbeddingModelsDir: () => join(testDir, "models"),
|
|
82
|
+
getSandboxRootDir: () => join(testDir, "sandbox"),
|
|
83
|
+
getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
|
|
84
|
+
getInterfacesDir: () => join(testDir, "interfaces"),
|
|
85
|
+
getSoundsDir: () => join(testDir, "sounds"),
|
|
86
|
+
getHistoryPath: () => join(testDir, "history"),
|
|
87
|
+
isMacOS: () => process.platform === "darwin",
|
|
88
|
+
isLinux: () => process.platform === "linux",
|
|
89
|
+
isWindows: () => process.platform === "win32",
|
|
90
|
+
getPlatformName: () => "linux",
|
|
91
|
+
getClipboardCommand: () => null,
|
|
92
|
+
resolveInstanceDataDir: () => undefined,
|
|
93
|
+
normalizeAssistantId: (id: string) => id,
|
|
94
|
+
getTCPPort: () => 0,
|
|
95
|
+
isTCPEnabled: () => false,
|
|
96
|
+
getTCPHost: () => "127.0.0.1",
|
|
97
|
+
isIOSPairingEnabled: () => false,
|
|
98
|
+
getPlatformTokenPath: () => join(testDir, "token"),
|
|
99
|
+
readPlatformToken: () => null,
|
|
100
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
101
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
102
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
103
|
+
getWorkspaceDirDisplay: () => testDir,
|
|
104
|
+
getWorkspacePromptPath: (file: string) => join(testDir, file),
|
|
105
|
+
ensureDataDir: () => {},
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
mock.module("../../../../config/loader.js", () => ({
|
|
109
|
+
API_KEY_PROVIDERS: [] as const,
|
|
110
|
+
getConfig: () => ({
|
|
111
|
+
permissions: { mode: "workspace" },
|
|
112
|
+
skills: { load: { extraDirs: [] } },
|
|
113
|
+
sandbox: { enabled: true },
|
|
114
|
+
}),
|
|
115
|
+
loadConfig: () => ({}),
|
|
116
|
+
invalidateConfigCache: () => {},
|
|
117
|
+
saveConfig: () => {},
|
|
118
|
+
loadRawConfig: () => ({}),
|
|
119
|
+
saveRawConfig: () => {},
|
|
120
|
+
getNestedValue: () => undefined,
|
|
121
|
+
setNestedValue: () => {},
|
|
122
|
+
applyNestedDefaults: (config: unknown) => config,
|
|
123
|
+
deepMergeMissing: () => false,
|
|
124
|
+
deepMergeOverwrite: () => {},
|
|
125
|
+
mergeDefaultWorkspaceConfig: () => {},
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Import module under test (after mocks are registered)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
const { buildCliProgram } = await import("../../../program.js");
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Test helper
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
async function runCommand(
|
|
139
|
+
args: string[],
|
|
140
|
+
): Promise<{ stdout: string; exitCode: number }> {
|
|
141
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
142
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
143
|
+
const stdoutChunks: string[] = [];
|
|
144
|
+
|
|
145
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
146
|
+
stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
147
|
+
return true;
|
|
148
|
+
}) as typeof process.stdout.write;
|
|
149
|
+
|
|
150
|
+
process.stderr.write = (() => true) as typeof process.stderr.write;
|
|
151
|
+
|
|
152
|
+
process.exitCode = 0;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const program = buildCliProgram();
|
|
156
|
+
program.exitOverride();
|
|
157
|
+
program.configureOutput({
|
|
158
|
+
writeErr: () => {},
|
|
159
|
+
writeOut: (str: string) => stdoutChunks.push(str),
|
|
160
|
+
});
|
|
161
|
+
await program.parseAsync(["node", "assistant", ...args]);
|
|
162
|
+
} catch {
|
|
163
|
+
if (process.exitCode === 0) process.exitCode = 1;
|
|
164
|
+
} finally {
|
|
165
|
+
process.stdout.write = originalStdoutWrite;
|
|
166
|
+
process.stderr.write = originalStderrWrite;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const exitCode = process.exitCode ?? 0;
|
|
170
|
+
process.exitCode = 0;
|
|
171
|
+
|
|
172
|
+
return { exitCode, stdout: stdoutChunks.join("") };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Tests
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe("assistant platform status", () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
mockGetSecureKeyViaDaemon = async () => undefined;
|
|
182
|
+
mockResolvePlatformCallbackRegistrationContext = async () => ({
|
|
183
|
+
containerized: false,
|
|
184
|
+
platformBaseUrl: "",
|
|
185
|
+
assistantId: "",
|
|
186
|
+
hasInternalApiKey: false,
|
|
187
|
+
hasAssistantApiKey: false,
|
|
188
|
+
authHeader: null,
|
|
189
|
+
enabled: false,
|
|
190
|
+
});
|
|
191
|
+
process.exitCode = 0;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("connected platform returns full status with stored credentials", async () => {
|
|
195
|
+
/**
|
|
196
|
+
* When the assistant has stored platform credentials and a valid
|
|
197
|
+
* registration context, the status command should report connected
|
|
198
|
+
* with all context fields populated.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
// GIVEN a containerized environment with platform configuration
|
|
202
|
+
mockResolvePlatformCallbackRegistrationContext = async () => ({
|
|
203
|
+
containerized: true,
|
|
204
|
+
platformBaseUrl: "https://platform.vellum.ai",
|
|
205
|
+
assistantId: "asst-abc-123",
|
|
206
|
+
hasInternalApiKey: true,
|
|
207
|
+
hasAssistantApiKey: true,
|
|
208
|
+
authHeader: "Bearer internal-key",
|
|
209
|
+
enabled: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// AND stored platform credentials exist
|
|
213
|
+
mockGetSecureKeyViaDaemon = async (account: string) => {
|
|
214
|
+
if (account === "credential/vellum/platform_base_url")
|
|
215
|
+
return "https://platform.vellum.ai";
|
|
216
|
+
if (account === "credential/vellum/assistant_api_key")
|
|
217
|
+
return "sk-test-key";
|
|
218
|
+
if (account === "credential/vellum/platform_organization_id")
|
|
219
|
+
return "org-456";
|
|
220
|
+
if (account === "credential/vellum/platform_user_id") return "user-789";
|
|
221
|
+
return undefined;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// WHEN the status command is run with --json
|
|
225
|
+
const { exitCode, stdout } = await runCommand([
|
|
226
|
+
"platform",
|
|
227
|
+
"status",
|
|
228
|
+
"--json",
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
// THEN the command succeeds
|
|
232
|
+
expect(exitCode).toBe(0);
|
|
233
|
+
|
|
234
|
+
// AND the output contains the expected status fields
|
|
235
|
+
const parsed = JSON.parse(stdout);
|
|
236
|
+
expect(parsed.containerized).toBe(true);
|
|
237
|
+
expect(parsed.baseUrl).toBe("https://platform.vellum.ai");
|
|
238
|
+
expect(parsed.assistantId).toBe("asst-abc-123");
|
|
239
|
+
expect(parsed.hasInternalApiKey).toBe(true);
|
|
240
|
+
expect(parsed.hasAssistantApiKey).toBe(true);
|
|
241
|
+
expect(parsed.available).toBe(true);
|
|
242
|
+
expect(parsed.connected).toBe(true);
|
|
243
|
+
expect(parsed.organizationId).toBe("org-456");
|
|
244
|
+
expect(parsed.userId).toBe("user-789");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -65,7 +65,7 @@ When the user asks to "connect my email", "set up email", "manage my email", or
|
|
|
65
65
|
- **confirmLabel:** "Get Started"
|
|
66
66
|
- **cancelLabel:** "Not Now"
|
|
67
67
|
- If the user confirms, briefly acknowledge (e.g., "Setting up Gmail now...") and proceed with the setup guide. If they decline, acknowledge and let them know they can set it up later.
|
|
68
|
-
3. **If the user provides a client_id directly in chat:** Call `credential_store` with `action: "oauth2_connect"`, `service: "google"`, and `client_id: "<their value>"`.
|
|
68
|
+
3. **If the user provides a client_id directly in chat:** Call `credential_store` with `action: "oauth2_connect"`, `service: "google"`, and `client_id: "<their value>"`. If a `client_secret` is also needed, collect it securely via `credential_store` with `action: "prompt"` — never accept it pasted in chat. Everything else is auto-filled.
|
|
69
69
|
|
|
70
70
|
### Slack
|
|
71
71
|
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"name": "navigate_settings_tab",
|
|
60
|
-
"description": "Open the Vellum settings panel to a specific tab (e.g. General,
|
|
60
|
+
"description": "Open the Vellum settings panel to a specific tab (e.g. General, Models & Services, Voice). Use this when the user needs to review or adjust settings visually.",
|
|
61
61
|
"category": "system",
|
|
62
62
|
"risk": "low",
|
|
63
63
|
"input_schema": {
|
|
@@ -67,11 +67,13 @@
|
|
|
67
67
|
"type": "string",
|
|
68
68
|
"enum": [
|
|
69
69
|
"General",
|
|
70
|
-
"Channels",
|
|
71
70
|
"Models & Services",
|
|
72
71
|
"Voice",
|
|
72
|
+
"Sounds",
|
|
73
73
|
"Permissions & Privacy",
|
|
74
|
-
"
|
|
74
|
+
"Billing",
|
|
75
|
+
"Archived Conversations",
|
|
76
|
+
"Schedules",
|
|
75
77
|
"Developer"
|
|
76
78
|
],
|
|
77
79
|
"description": "The settings tab to navigate to"
|
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
|
|
6
6
|
const SETTINGS_TABS = [
|
|
7
7
|
"General",
|
|
8
|
-
"Channels",
|
|
9
8
|
"Models & Services",
|
|
10
9
|
"Voice",
|
|
10
|
+
"Sounds",
|
|
11
11
|
"Permissions & Privacy",
|
|
12
|
-
"
|
|
12
|
+
"Billing",
|
|
13
|
+
"Archived Conversations",
|
|
14
|
+
"Schedules",
|
|
13
15
|
"Developer",
|
|
14
16
|
] as const;
|
|
15
17
|
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"key": "app-builder-multifile",
|
|
48
48
|
"label": "App Builder Multi-file",
|
|
49
49
|
"description": "Enable multi-file TSX app creation with esbuild compilation instead of single-HTML apps",
|
|
50
|
-
"defaultEnabled":
|
|
50
|
+
"defaultEnabled": true
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"id": "mobile-pairing",
|
|
@@ -88,6 +88,12 @@ export interface CesClientHandshakeOptions {
|
|
|
88
88
|
* credential materialisation without relying on env vars.
|
|
89
89
|
*/
|
|
90
90
|
assistantApiKey?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Optional platform assistant ID to pass to CES during the handshake.
|
|
93
|
+
* For warm-pool pods the PLATFORM_ASSISTANT_ID env var is empty at CES
|
|
94
|
+
* startup, so the assistant forwards it here once it is known.
|
|
95
|
+
*/
|
|
96
|
+
assistantId?: string;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export interface CesClient {
|
|
@@ -111,13 +117,16 @@ export interface CesClient {
|
|
|
111
117
|
): Promise<CesRpcContract[M]["response"]>;
|
|
112
118
|
|
|
113
119
|
/**
|
|
114
|
-
* Push an updated assistant API key to CES.
|
|
120
|
+
* Push an updated assistant API key (and optionally assistant ID) to CES.
|
|
115
121
|
*
|
|
116
122
|
* In managed mode the API key is provisioned after hatch, so the initial
|
|
117
123
|
* handshake may have been sent without one. This method pushes the key
|
|
118
124
|
* to CES after it arrives, without requiring a re-handshake.
|
|
119
125
|
*/
|
|
120
|
-
updateAssistantApiKey(
|
|
126
|
+
updateAssistantApiKey(
|
|
127
|
+
assistantApiKey: string,
|
|
128
|
+
assistantId?: string,
|
|
129
|
+
): Promise<{ updated: boolean }>;
|
|
121
130
|
|
|
122
131
|
/** Whether the client has completed a successful handshake. */
|
|
123
132
|
isReady(): boolean;
|
|
@@ -312,6 +321,7 @@ export function createCesClient(
|
|
|
312
321
|
...(options?.assistantApiKey
|
|
313
322
|
? { assistantApiKey: options.assistantApiKey }
|
|
314
323
|
: {}),
|
|
324
|
+
...(options?.assistantId ? { assistantId: options.assistantId } : {}),
|
|
315
325
|
});
|
|
316
326
|
} catch (err) {
|
|
317
327
|
const entry = pending.get("handshake");
|
|
@@ -341,9 +351,11 @@ export function createCesClient(
|
|
|
341
351
|
|
|
342
352
|
async updateAssistantApiKey(
|
|
343
353
|
assistantApiKey: string,
|
|
354
|
+
assistantId?: string,
|
|
344
355
|
): Promise<{ updated: boolean }> {
|
|
345
356
|
return call(CesRpcMethod.UpdateManagedCredential, {
|
|
346
357
|
assistantApiKey,
|
|
358
|
+
...(assistantId ? { assistantId } : {}),
|
|
347
359
|
});
|
|
348
360
|
},
|
|
349
361
|
|
|
@@ -22,7 +22,12 @@ export function isWakeUpGreeting(
|
|
|
22
22
|
): boolean {
|
|
23
23
|
if (conversationMessageCount !== 0) return false;
|
|
24
24
|
if (!existsSync(getWorkspacePromptPath("BOOTSTRAP.md"))) return false;
|
|
25
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
content
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[.!?]+$/, "") === "wake up, my friend"
|
|
30
|
+
);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { setRelayBroadcast } from "../calls/relay-server.js";
|
|
|
8
8
|
import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
|
|
9
9
|
import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
|
|
10
10
|
import {
|
|
11
|
+
getPlatformAssistantId,
|
|
11
12
|
getQdrantHttpPortEnv,
|
|
12
13
|
getQdrantUrlEnv,
|
|
13
14
|
getRuntimeHttpHost,
|
|
@@ -198,11 +199,13 @@ export async function startCesProcess(
|
|
|
198
199
|
// after hatch and stored in the credential store — CES can't read
|
|
199
200
|
// the env var, so we pass it via the handshake.
|
|
200
201
|
const proxyCtx = await resolveManagedProxyContext();
|
|
201
|
-
const
|
|
202
|
-
|
|
202
|
+
const assistantId = getPlatformAssistantId();
|
|
203
|
+
const { accepted, reason } = await client.handshake({
|
|
204
|
+
...(proxyCtx.assistantApiKey
|
|
203
205
|
? { assistantApiKey: proxyCtx.assistantApiKey }
|
|
204
|
-
:
|
|
205
|
-
|
|
206
|
+
: {}),
|
|
207
|
+
...(assistantId ? { assistantId } : {}),
|
|
208
|
+
});
|
|
206
209
|
if (abortController.signal.aborted) {
|
|
207
210
|
client.close();
|
|
208
211
|
throw new Error("CES initialization aborted during shutdown");
|
|
@@ -511,11 +514,13 @@ export async function runDaemon(): Promise<void> {
|
|
|
511
514
|
const transport = await pm.start();
|
|
512
515
|
const newClient = createCesClient(transport);
|
|
513
516
|
const proxyCtx = await resolveManagedProxyContext();
|
|
514
|
-
const
|
|
515
|
-
|
|
517
|
+
const assistantId = getPlatformAssistantId();
|
|
518
|
+
const { accepted, reason } = await newClient.handshake({
|
|
519
|
+
...(proxyCtx.assistantApiKey
|
|
516
520
|
? { assistantApiKey: proxyCtx.assistantApiKey }
|
|
517
|
-
:
|
|
518
|
-
|
|
521
|
+
: {}),
|
|
522
|
+
...(assistantId ? { assistantId } : {}),
|
|
523
|
+
});
|
|
519
524
|
if (accepted) {
|
|
520
525
|
log.info("CES reconnection handshake accepted");
|
|
521
526
|
return newClient;
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { buildCliProgram } from "./cli/program.js";
|
|
4
|
-
import { resolveInstanceDataDir } from "./util/platform.js";
|
|
5
|
-
|
|
6
|
-
// Auto-resolve BASE_DATA_DIR from the lockfile when running as a standalone CLI.
|
|
7
|
-
// The daemon always has BASE_DATA_DIR set by the launcher (cli/src/lib/local.ts),
|
|
8
|
-
// but the CLI process doesn't — so credential commands and other path-dependent
|
|
9
|
-
// operations would read from ~/.vellum instead of the instance-scoped directory.
|
|
10
|
-
if (!process.env.BASE_DATA_DIR) {
|
|
11
|
-
const instanceDir = resolveInstanceDataDir();
|
|
12
|
-
if (instanceDir) {
|
|
13
|
-
process.env.BASE_DATA_DIR = instanceDir;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
4
|
|
|
17
5
|
buildCliProgram().parse();
|
|
@@ -26,13 +26,13 @@ function buildFtsMatchQuery(text: string): string | null {
|
|
|
26
26
|
|
|
27
27
|
export function listConversations(
|
|
28
28
|
limit?: number,
|
|
29
|
-
|
|
29
|
+
backgroundOnly = false,
|
|
30
30
|
offset = 0,
|
|
31
31
|
): ConversationRow[] {
|
|
32
32
|
ensureDisplayOrderMigration();
|
|
33
33
|
const db = getDb();
|
|
34
|
-
const where =
|
|
35
|
-
?
|
|
34
|
+
const where = backgroundOnly
|
|
35
|
+
? sql`${conversations.conversationType} = 'background'`
|
|
36
36
|
: sql`${conversations.conversationType} NOT IN ('background', 'private')`;
|
|
37
37
|
const query = db
|
|
38
38
|
.select()
|
|
@@ -44,10 +44,10 @@ export function listConversations(
|
|
|
44
44
|
return query.all().map(parseConversation);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function countConversations(
|
|
47
|
+
export function countConversations(backgroundOnly = false): number {
|
|
48
48
|
const db = getDb();
|
|
49
|
-
const where =
|
|
50
|
-
?
|
|
49
|
+
const where = backgroundOnly
|
|
50
|
+
? sql`${conversations.conversationType} = 'background'`
|
|
51
51
|
: sql`${conversations.conversationType} NOT IN ('background', 'private')`;
|
|
52
52
|
const [{ total }] = db
|
|
53
53
|
.select({ total: count() })
|
|
@@ -143,7 +143,10 @@ export function upsertJournalMemoriesFromDisk(
|
|
|
143
143
|
|
|
144
144
|
// Filter for .md files, excluding readme.md (case-insensitive)
|
|
145
145
|
const mdFiles = files.filter(
|
|
146
|
-
(f) =>
|
|
146
|
+
(f) =>
|
|
147
|
+
f.endsWith(".md") &&
|
|
148
|
+
!f.startsWith(".") &&
|
|
149
|
+
f.toLowerCase() !== "readme.md",
|
|
147
150
|
);
|
|
148
151
|
|
|
149
152
|
let upserted = 0;
|
|
@@ -178,7 +181,10 @@ export function upsertJournalMemoriesFromDisk(
|
|
|
178
181
|
if (!statSync(subdirPath).isDirectory()) continue;
|
|
179
182
|
|
|
180
183
|
const subFiles = readdirSync(subdirPath).filter(
|
|
181
|
-
(f) =>
|
|
184
|
+
(f) =>
|
|
185
|
+
f.endsWith(".md") &&
|
|
186
|
+
!f.startsWith(".") &&
|
|
187
|
+
f.toLowerCase() !== "readme.md",
|
|
182
188
|
);
|
|
183
189
|
|
|
184
190
|
for (const filename of subFiles) {
|
|
@@ -78,7 +78,10 @@ export function buildJournalContext(
|
|
|
78
78
|
|
|
79
79
|
// Filter for .md files, excluding README.md (case-insensitive)
|
|
80
80
|
const mdFiles = files.filter(
|
|
81
|
-
(f) =>
|
|
81
|
+
(f) =>
|
|
82
|
+
f.endsWith(".md") &&
|
|
83
|
+
!f.startsWith(".") &&
|
|
84
|
+
f.toLowerCase() !== "readme.md",
|
|
82
85
|
);
|
|
83
86
|
|
|
84
87
|
// Collect file info with birthtime (creation time), skipping unreadable entries
|
|
@@ -197,6 +197,7 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
|
|
|
197
197
|
// System Permissions section removed — guidance lives in request_system_permission tool description.
|
|
198
198
|
// Parallel Task Orchestration section removed — orchestration skill description + hints cover this.
|
|
199
199
|
staticParts.push(buildAccessPreferenceSection(hasNoClient));
|
|
200
|
+
staticParts.push(buildCredentialSecuritySection());
|
|
200
201
|
// Memory Persistence, Memory Recall, Workspace Reflection, Learning from Mistakes
|
|
201
202
|
// sections removed — guidance lives in memory_manage/memory_recall tool descriptions
|
|
202
203
|
// and the Proactive Workspace Editing subsection in Configuration.
|
|
@@ -309,6 +310,8 @@ function buildInChatConfigurationSection(): string {
|
|
|
309
310
|
"## In-Chat Configuration",
|
|
310
311
|
"",
|
|
311
312
|
"When the user needs to configure a value, collect it conversationally in the chat. Never direct the user to the Settings page for initial setup - Settings is for reviewing and updating existing configuration.",
|
|
313
|
+
"",
|
|
314
|
+
'The Settings tabs are: General, Models & Services, Voice, Sounds, Permissions & Privacy, Billing, Archived Conversations, Schedules, Developer. There is NO "Integrations" tab — never refer to "Settings > Integrations". For API keys and provider configuration, the correct tab is "Models & Services".',
|
|
312
315
|
].join("\n");
|
|
313
316
|
}
|
|
314
317
|
|
|
@@ -334,6 +337,14 @@ function buildAccessPreferenceSection(hasNoClient: boolean): string {
|
|
|
334
337
|
].join("\n");
|
|
335
338
|
}
|
|
336
339
|
|
|
340
|
+
function buildCredentialSecuritySection(): string {
|
|
341
|
+
return [
|
|
342
|
+
"## Credential Security",
|
|
343
|
+
"",
|
|
344
|
+
'Never ask users to share secrets (API keys, tokens, passwords, webhook secrets) in chat — secret messages may be blocked at ingress. Use the `credential_store` tool with `action: "prompt"` instead; it collects secrets through a secure UI that never exposes the value in the conversation. Non-secret values (Client IDs, Account SIDs, usernames) may be collected conversationally.',
|
|
345
|
+
].join("\n");
|
|
346
|
+
}
|
|
347
|
+
|
|
337
348
|
function buildIntegrationSection(): string {
|
|
338
349
|
let connections: { providerKey: string; accountInfo?: string | null }[];
|
|
339
350
|
try {
|
|
@@ -27,13 +27,11 @@ import {
|
|
|
27
27
|
handleVoiceWebhook,
|
|
28
28
|
} from "../calls/twilio-routes.js";
|
|
29
29
|
import { parseChannelId } from "../channels/types.js";
|
|
30
|
-
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
31
30
|
import {
|
|
32
31
|
getGatewayInternalBaseUrl,
|
|
33
32
|
hasUngatedHttpAuthDisabled,
|
|
34
33
|
isHttpAuthDisabled,
|
|
35
34
|
} from "../config/env.js";
|
|
36
|
-
import { getConfig } from "../config/loader.js";
|
|
37
35
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
38
36
|
import { PairingStore } from "../daemon/pairing-store.js";
|
|
39
37
|
import {
|
|
@@ -1030,17 +1028,11 @@ export class RuntimeHttpServer {
|
|
|
1030
1028
|
handler: ({ url }) => {
|
|
1031
1029
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
1032
1030
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
1033
|
-
const
|
|
1034
|
-
"
|
|
1035
|
-
|
|
1036
|
-
);
|
|
1037
|
-
const
|
|
1038
|
-
limit,
|
|
1039
|
-
includeBackground,
|
|
1040
|
-
offset,
|
|
1041
|
-
);
|
|
1042
|
-
const totalCount = countConversations(includeBackground);
|
|
1043
|
-
const conversationIds = conversations.map((c) => c.id);
|
|
1031
|
+
const backgroundOnly =
|
|
1032
|
+
url.searchParams.get("conversationType") === "background";
|
|
1033
|
+
const rows = listConversations(limit, backgroundOnly, offset);
|
|
1034
|
+
const totalCount = countConversations(backgroundOnly);
|
|
1035
|
+
const conversationIds = rows.map((c) => c.id);
|
|
1044
1036
|
const displayMeta = getDisplayMetaForConversations(conversationIds);
|
|
1045
1037
|
const bindings =
|
|
1046
1038
|
externalConversationStore.getBindingsForConversations(
|
|
@@ -1050,7 +1042,7 @@ export class RuntimeHttpServer {
|
|
|
1050
1042
|
getAttentionStateByConversationIds(conversationIds);
|
|
1051
1043
|
const parentCache = new Map<string, ConversationRow | null>();
|
|
1052
1044
|
return Response.json({
|
|
1053
|
-
conversations:
|
|
1045
|
+
conversations: rows.map((conversation) =>
|
|
1054
1046
|
this.serializeConversationSummary({
|
|
1055
1047
|
conversation,
|
|
1056
1048
|
binding: bindings.get(conversation.id),
|
|
@@ -1059,7 +1051,7 @@ export class RuntimeHttpServer {
|
|
|
1059
1051
|
parentCache,
|
|
1060
1052
|
}),
|
|
1061
1053
|
),
|
|
1062
|
-
hasMore: offset +
|
|
1054
|
+
hasMore: offset + rows.length < totalCount,
|
|
1063
1055
|
});
|
|
1064
1056
|
},
|
|
1065
1057
|
},
|
|
@@ -106,7 +106,7 @@ const TASK_DEFINITIONS: readonly TaskDefinition[] = [
|
|
|
106
106
|
"Secrets are redacted in export bundles for security. Re-enter all API keys, tokens, and credentials in the destination instance.",
|
|
107
107
|
required: true,
|
|
108
108
|
helpText:
|
|
109
|
-
"Navigate to Settings >
|
|
109
|
+
"Navigate to Settings > Models & Services to re-enter provider API keys (e.g., Anthropic, OpenAI). Check Settings > Models & Services for any custom secrets used by skills.",
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
id: "rebind-channels",
|
|
@@ -133,7 +133,7 @@ const TASK_DEFINITIONS: readonly TaskDefinition[] = [
|
|
|
133
133
|
"Ensure all webhook URLs registered with external services point to the new instance's public ingress URL.",
|
|
134
134
|
required: false,
|
|
135
135
|
helpText:
|
|
136
|
-
"Review the public ingress URL in Settings >
|
|
136
|
+
"Review the public ingress URL in Settings > Developer. Update any external services (GitHub, calendar providers, etc.) that send webhooks to this assistant.",
|
|
137
137
|
},
|
|
138
138
|
] as const;
|
|
139
139
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
getPlatformAssistantId,
|
|
4
5
|
setPlatformAssistantId,
|
|
5
6
|
setPlatformBaseUrl,
|
|
6
7
|
setPlatformOrganizationId,
|
|
@@ -86,7 +87,10 @@ async function queueApiKeyPropagation(
|
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
try {
|
|
89
|
-
await cesClient.updateAssistantApiKey(
|
|
90
|
+
await cesClient.updateAssistantApiKey(
|
|
91
|
+
apiKey,
|
|
92
|
+
getPlatformAssistantId() || undefined,
|
|
93
|
+
);
|
|
90
94
|
log.info(
|
|
91
95
|
"Pushed queued assistant API key to CES after handshake completed",
|
|
92
96
|
);
|
|
@@ -282,7 +286,10 @@ export async function handleAddSecret(
|
|
|
282
286
|
if (cesClient) {
|
|
283
287
|
if (cesClient.isReady()) {
|
|
284
288
|
try {
|
|
285
|
-
await cesClient.updateAssistantApiKey(
|
|
289
|
+
await cesClient.updateAssistantApiKey(
|
|
290
|
+
value,
|
|
291
|
+
getPlatformAssistantId() || undefined,
|
|
292
|
+
);
|
|
286
293
|
log.info(
|
|
287
294
|
"Pushed assistant API key to CES after managed proxy credential update",
|
|
288
295
|
);
|
|
@@ -242,13 +242,13 @@ class BrowserManager {
|
|
|
242
242
|
if (!chromiumInstalled) {
|
|
243
243
|
log.info("Chromium not installed, installing via playwright...");
|
|
244
244
|
const proc = Bun.spawn(
|
|
245
|
-
["bunx", "playwright", "install", "chromium"],
|
|
245
|
+
["bunx", "playwright", "install", "--with-deps", "chromium"],
|
|
246
246
|
{
|
|
247
247
|
stdout: "pipe",
|
|
248
248
|
stderr: "pipe",
|
|
249
249
|
},
|
|
250
250
|
);
|
|
251
|
-
const timeoutMs =
|
|
251
|
+
const timeoutMs = 300_000;
|
|
252
252
|
let timer: ReturnType<typeof setTimeout>;
|
|
253
253
|
const exitCode = await Promise.race([
|
|
254
254
|
proc.exited.finally(() => clearTimeout(timer)),
|