@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.
Files changed (31) hide show
  1. package/Dockerfile +41 -9
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  3. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  4. package/openapi.yaml +1 -1
  5. package/package.json +1 -1
  6. package/src/__tests__/first-greeting.test.ts +7 -0
  7. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  8. package/src/__tests__/platform.test.ts +3 -168
  9. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  10. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  11. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  12. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  13. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  14. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  15. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  16. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  17. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  18. package/src/config/feature-flag-registry.json +1 -1
  19. package/src/credential-execution/client.ts +14 -2
  20. package/src/daemon/first-greeting.ts +6 -1
  21. package/src/daemon/lifecycle.ts +13 -8
  22. package/src/index.ts +0 -12
  23. package/src/memory/conversation-queries.ts +6 -6
  24. package/src/memory/journal-memory.ts +8 -2
  25. package/src/prompts/journal-context.ts +4 -1
  26. package/src/prompts/system-prompt.ts +11 -0
  27. package/src/runtime/http-server.ts +7 -15
  28. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  29. package/src/runtime/routes/secret-routes.ts +9 -2
  30. package/src/tools/browser/browser-manager.ts +2 -2
  31. 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>"`. Include `client_secret` too if they provide one. Everything else is auto-filled.
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, Channels, Voice). Use this when the user needs to review or adjust settings visually.",
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
- "Contacts",
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
- "Contacts",
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": false
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(assistantApiKey: string): Promise<{ updated: boolean }>;
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 content.trim().toLowerCase() === "wake up, my friend.";
25
+ return (
26
+ content
27
+ .trim()
28
+ .toLowerCase()
29
+ .replace(/[.!?]+$/, "") === "wake up, my friend"
30
+ );
26
31
  }
27
32
 
28
33
  /**
@@ -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 { accepted, reason } = await client.handshake(
202
- proxyCtx.assistantApiKey
202
+ const assistantId = getPlatformAssistantId();
203
+ const { accepted, reason } = await client.handshake({
204
+ ...(proxyCtx.assistantApiKey
203
205
  ? { assistantApiKey: proxyCtx.assistantApiKey }
204
- : undefined,
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 { accepted, reason } = await newClient.handshake(
515
- proxyCtx.assistantApiKey
517
+ const assistantId = getPlatformAssistantId();
518
+ const { accepted, reason } = await newClient.handshake({
519
+ ...(proxyCtx.assistantApiKey
516
520
  ? { assistantApiKey: proxyCtx.assistantApiKey }
517
- : undefined,
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
- includeBackground = false,
29
+ backgroundOnly = false,
30
30
  offset = 0,
31
31
  ): ConversationRow[] {
32
32
  ensureDisplayOrderMigration();
33
33
  const db = getDb();
34
- const where = includeBackground
35
- ? undefined
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(includeBackground = false): number {
47
+ export function countConversations(backgroundOnly = false): number {
48
48
  const db = getDb();
49
- const where = includeBackground
50
- ? undefined
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) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
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) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
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) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
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 includeBackground = isAssistantFeatureFlagEnabled(
1034
- "show-background-conversations",
1035
- getConfig(),
1036
- );
1037
- const conversations = listConversations(
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: conversations.map((conversation) =>
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 + conversations.length < totalCount,
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 > Integrations to re-enter provider API keys (e.g., Anthropic, OpenAI). Check Settings > Secrets for any custom secrets used by skills.",
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 > Gateway. Update any external services (GitHub, calendar providers, etc.) that send webhooks to this assistant.",
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(apiKey);
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(value);
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 = 120_000;
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)),