@vellumai/assistant 0.5.11 → 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 +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- 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/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- 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/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -57,8 +57,8 @@ describe("onboarding template contracts", () => {
|
|
|
57
57
|
test("contains wrapping-up criteria with required conditions", () => {
|
|
58
58
|
const lower = bootstrap.toLowerCase();
|
|
59
59
|
expect(lower).toContain("wrapping up");
|
|
60
|
-
expect(lower).toContain("
|
|
61
|
-
expect(lower).toContain("
|
|
60
|
+
expect(lower).toContain("delete");
|
|
61
|
+
expect(lower).toContain("bootstrap.md");
|
|
62
62
|
expect(lower).toContain("two suggestions");
|
|
63
63
|
});
|
|
64
64
|
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { afterEach, describe, expect,
|
|
6
|
-
|
|
7
|
-
// Mutable homedir override — when set, resolveInstanceDataDir reads from here.
|
|
8
|
-
let homedirOverride: string | undefined;
|
|
9
|
-
mock.module("node:os", () => ({
|
|
10
|
-
homedir: () => homedirOverride ?? homedir(),
|
|
11
|
-
tmpdir,
|
|
12
|
-
}));
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
13
6
|
|
|
14
7
|
import {
|
|
15
8
|
ensureDataDir,
|
|
@@ -28,7 +21,6 @@ import {
|
|
|
28
21
|
getWorkspaceHooksDir,
|
|
29
22
|
getWorkspacePromptPath,
|
|
30
23
|
getWorkspaceSkillsDir,
|
|
31
|
-
resolveInstanceDataDir,
|
|
32
24
|
} from "../util/platform.js";
|
|
33
25
|
|
|
34
26
|
const originalBaseDataDir = process.env.BASE_DATA_DIR;
|
|
@@ -39,7 +31,6 @@ afterEach(() => {
|
|
|
39
31
|
} else {
|
|
40
32
|
process.env.BASE_DATA_DIR = originalBaseDataDir;
|
|
41
33
|
}
|
|
42
|
-
homedirOverride = undefined;
|
|
43
34
|
});
|
|
44
35
|
|
|
45
36
|
// Baseline path characterization: documents current pre-migration path layout.
|
|
@@ -155,159 +146,3 @@ describe("workspace path primitives", () => {
|
|
|
155
146
|
expect(getWorkspaceDir()).toBe("/tmp/custom-base/.vellum/workspace");
|
|
156
147
|
});
|
|
157
148
|
});
|
|
158
|
-
|
|
159
|
-
describe("resolveInstanceDataDir", () => {
|
|
160
|
-
function makeTempHome(): string {
|
|
161
|
-
const dir = join(
|
|
162
|
-
tmpdir(),
|
|
163
|
-
`platform-home-${randomBytes(4).toString("hex")}`,
|
|
164
|
-
);
|
|
165
|
-
mkdirSync(dir, { recursive: true });
|
|
166
|
-
homedirOverride = dir;
|
|
167
|
-
return dir;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function writeLockfileToHome(
|
|
171
|
-
home: string,
|
|
172
|
-
data: Record<string, unknown>,
|
|
173
|
-
): void {
|
|
174
|
-
writeFileSync(
|
|
175
|
-
join(home, ".vellum.lock.json"),
|
|
176
|
-
JSON.stringify(data, null, 2),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
test("returns undefined when no lockfile exists", () => {
|
|
181
|
-
makeTempHome();
|
|
182
|
-
expect(resolveInstanceDataDir()).toBeUndefined();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("returns sole local assistant instanceDir when no activeAssistant", () => {
|
|
186
|
-
const home = makeTempHome();
|
|
187
|
-
writeLockfileToHome(home, {
|
|
188
|
-
assistants: [
|
|
189
|
-
{
|
|
190
|
-
assistantId: "vellum-calm-stork",
|
|
191
|
-
cloud: "local",
|
|
192
|
-
resources: {
|
|
193
|
-
instanceDir:
|
|
194
|
-
"/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
});
|
|
199
|
-
expect(resolveInstanceDataDir()).toBe(
|
|
200
|
-
"/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("returns active assistant instanceDir when activeAssistant matches", () => {
|
|
205
|
-
const home = makeTempHome();
|
|
206
|
-
writeLockfileToHome(home, {
|
|
207
|
-
activeAssistant: "vellum-bold-fox",
|
|
208
|
-
assistants: [
|
|
209
|
-
{
|
|
210
|
-
assistantId: "vellum-calm-stork",
|
|
211
|
-
cloud: "local",
|
|
212
|
-
resources: {
|
|
213
|
-
instanceDir:
|
|
214
|
-
"/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
assistantId: "vellum-bold-fox",
|
|
219
|
-
cloud: "local",
|
|
220
|
-
resources: {
|
|
221
|
-
instanceDir:
|
|
222
|
-
"/Users/test/.local/share/vellum/assistants/vellum-bold-fox",
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
});
|
|
227
|
-
expect(resolveInstanceDataDir()).toBe(
|
|
228
|
-
"/Users/test/.local/share/vellum/assistants/vellum-bold-fox",
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("returns undefined when multiple local assistants and no activeAssistant", () => {
|
|
233
|
-
const home = makeTempHome();
|
|
234
|
-
writeLockfileToHome(home, {
|
|
235
|
-
assistants: [
|
|
236
|
-
{
|
|
237
|
-
assistantId: "vellum-calm-stork",
|
|
238
|
-
cloud: "local",
|
|
239
|
-
resources: {
|
|
240
|
-
instanceDir:
|
|
241
|
-
"/Users/test/.local/share/vellum/assistants/vellum-calm-stork",
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
assistantId: "vellum-bold-fox",
|
|
246
|
-
cloud: "local",
|
|
247
|
-
resources: {
|
|
248
|
-
instanceDir:
|
|
249
|
-
"/Users/test/.local/share/vellum/assistants/vellum-bold-fox",
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
});
|
|
254
|
-
expect(resolveInstanceDataDir()).toBeUndefined();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("returns undefined when lockfile has no assistants array", () => {
|
|
258
|
-
const home = makeTempHome();
|
|
259
|
-
writeLockfileToHome(home, { version: 1 });
|
|
260
|
-
expect(resolveInstanceDataDir()).toBeUndefined();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("returns undefined when lockfile is malformed JSON", () => {
|
|
264
|
-
const home = makeTempHome();
|
|
265
|
-
writeFileSync(join(home, ".vellum.lock.json"), "{{not json");
|
|
266
|
-
expect(resolveInstanceDataDir()).toBeUndefined();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("treats assistants without cloud field as local", () => {
|
|
270
|
-
const home = makeTempHome();
|
|
271
|
-
writeLockfileToHome(home, {
|
|
272
|
-
assistants: [
|
|
273
|
-
{
|
|
274
|
-
assistantId: "vellum-quiet-owl",
|
|
275
|
-
resources: {
|
|
276
|
-
instanceDir:
|
|
277
|
-
"/Users/test/.local/share/vellum/assistants/vellum-quiet-owl",
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
});
|
|
282
|
-
expect(resolveInstanceDataDir()).toBe(
|
|
283
|
-
"/Users/test/.local/share/vellum/assistants/vellum-quiet-owl",
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("ignores cloud assistants when resolving", () => {
|
|
288
|
-
const home = makeTempHome();
|
|
289
|
-
writeLockfileToHome(home, {
|
|
290
|
-
assistants: [
|
|
291
|
-
{
|
|
292
|
-
assistantId: "vellum-cloud-eagle",
|
|
293
|
-
cloud: "platform",
|
|
294
|
-
resources: {
|
|
295
|
-
instanceDir: "/some/cloud/path",
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
assistantId: "vellum-local-robin",
|
|
300
|
-
cloud: "local",
|
|
301
|
-
resources: {
|
|
302
|
-
instanceDir:
|
|
303
|
-
"/Users/test/.local/share/vellum/assistants/vellum-local-robin",
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
],
|
|
307
|
-
});
|
|
308
|
-
// Only one local assistant, so it auto-selects
|
|
309
|
-
expect(resolveInstanceDataDir()).toBe(
|
|
310
|
-
"/Users/test/.local/share/vellum/assistants/vellum-local-robin",
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
});
|
|
@@ -6,6 +6,7 @@ let lastGeminiConstructorOpts: Record<string, unknown> | null = null;
|
|
|
6
6
|
let secureKeyStore: Record<string, string | undefined> = {};
|
|
7
7
|
const metadataUpserts: Array<{ service: string; field: string }> = [];
|
|
8
8
|
const metadataDeletes: Array<{ service: string; field: string }> = [];
|
|
9
|
+
let providerRefreshCalls = 0;
|
|
9
10
|
|
|
10
11
|
const PLATFORM_BASE_URL = "https://platform.example.com";
|
|
11
12
|
const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
|
|
@@ -137,6 +138,29 @@ function makeDeleteCredentialRequest(name: string): Request {
|
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
function makeAddApiKeyRequest(name: string, value: string): Request {
|
|
142
|
+
return new Request("http://localhost/v1/secrets", {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
type: "api_key",
|
|
147
|
+
name,
|
|
148
|
+
value,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function makeDeleteApiKeyRequest(name: string): Request {
|
|
154
|
+
return new Request("http://localhost/v1/secrets", {
|
|
155
|
+
method: "DELETE",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
type: "api_key",
|
|
159
|
+
name,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
describe("secret routes managed proxy registry sync", () => {
|
|
141
165
|
beforeEach(async () => {
|
|
142
166
|
secureKeyStore = {};
|
|
@@ -144,6 +168,7 @@ describe("secret routes managed proxy registry sync", () => {
|
|
|
144
168
|
metadataDeletes.length = 0;
|
|
145
169
|
lastGeminiConstructorOpts = null;
|
|
146
170
|
platformBaseUrlOverride = undefined;
|
|
171
|
+
providerRefreshCalls = 0;
|
|
147
172
|
await initializeProviders(mockConfig);
|
|
148
173
|
});
|
|
149
174
|
|
|
@@ -169,6 +194,33 @@ describe("secret routes managed proxy registry sync", () => {
|
|
|
169
194
|
expect(lastGeminiConstructorOpts).toBeDefined();
|
|
170
195
|
});
|
|
171
196
|
|
|
197
|
+
test("provider API key writes notify live-conversation refresh listeners", async () => {
|
|
198
|
+
const res = await handleAddSecret(
|
|
199
|
+
makeAddApiKeyRequest("fireworks", "fw-key"),
|
|
200
|
+
{
|
|
201
|
+
onProviderCredentialsChanged: () => {
|
|
202
|
+
providerRefreshCalls++;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(res.status).toBe(201);
|
|
208
|
+
expect(secureKeyStore.fireworks).toBe("fw-key");
|
|
209
|
+
expect(providerRefreshCalls).toBe(1);
|
|
210
|
+
|
|
211
|
+
const deleteRes = await handleDeleteSecret(
|
|
212
|
+
makeDeleteApiKeyRequest("fireworks"),
|
|
213
|
+
{
|
|
214
|
+
onProviderCredentialsChanged: () => {
|
|
215
|
+
providerRefreshCalls++;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(deleteRes.status).toBe(200);
|
|
221
|
+
expect(providerRefreshCalls).toBe(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
172
224
|
test("deleting vellum:assistant_api_key clears managed fallback providers immediately", async () => {
|
|
173
225
|
secureKeyStore[ASSISTANT_API_KEY_PATH] = "ast-managed-key";
|
|
174
226
|
await initializeProviders(mockConfig);
|
|
@@ -190,6 +242,32 @@ describe("secret routes managed proxy registry sync", () => {
|
|
|
190
242
|
expect(listProviders()).toEqual([]);
|
|
191
243
|
});
|
|
192
244
|
|
|
245
|
+
test("managed proxy credential writes notify live-conversation refresh listeners", async () => {
|
|
246
|
+
const res = await handleAddSecret(
|
|
247
|
+
makeAddCredentialRequest("vellum:assistant_api_key", "ast-managed-key"),
|
|
248
|
+
{
|
|
249
|
+
onProviderCredentialsChanged: () => {
|
|
250
|
+
providerRefreshCalls++;
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(res.status).toBe(201);
|
|
256
|
+
expect(providerRefreshCalls).toBe(1);
|
|
257
|
+
|
|
258
|
+
const deleteRes = await handleDeleteSecret(
|
|
259
|
+
makeDeleteCredentialRequest("vellum:assistant_api_key"),
|
|
260
|
+
{
|
|
261
|
+
onProviderCredentialsChanged: () => {
|
|
262
|
+
providerRefreshCalls++;
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(deleteRes.status).toBe(200);
|
|
268
|
+
expect(providerRefreshCalls).toBe(2);
|
|
269
|
+
});
|
|
270
|
+
|
|
193
271
|
test("storing vellum:platform_base_url sets override and triggers initializeProviders", async () => {
|
|
194
272
|
const res = await handleAddSecret(
|
|
195
273
|
makeAddCredentialRequest(
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../util/logger.js", () => ({
|
|
4
|
+
getLogger: () =>
|
|
5
|
+
new Proxy({} as Record<string, unknown>, {
|
|
6
|
+
get: () => () => {},
|
|
7
|
+
}),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import type { CesClient } from "../credential-execution/client.js";
|
|
11
|
+
import {
|
|
12
|
+
_resetBackend,
|
|
13
|
+
getActiveBackendName,
|
|
14
|
+
getSecureKeyAsync,
|
|
15
|
+
setCesClient,
|
|
16
|
+
} from "../security/secure-keys.js";
|
|
17
|
+
|
|
18
|
+
const rpcCall = mock(async () => ({ found: false }));
|
|
19
|
+
const originalFetch = globalThis.fetch;
|
|
20
|
+
|
|
21
|
+
let rpcReady = true;
|
|
22
|
+
|
|
23
|
+
function createMockCesClient(): CesClient {
|
|
24
|
+
return {
|
|
25
|
+
handshake: mock(async () => ({ accepted: true })),
|
|
26
|
+
call: rpcCall as CesClient["call"],
|
|
27
|
+
updateAssistantApiKey: mock(async () => ({ updated: true })),
|
|
28
|
+
isReady: () => rpcReady,
|
|
29
|
+
close: mock(() => {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("secure-keys managed CES failover", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
_resetBackend();
|
|
36
|
+
rpcCall.mockClear();
|
|
37
|
+
rpcReady = true;
|
|
38
|
+
process.env.IS_CONTAINERIZED = "1";
|
|
39
|
+
process.env.CES_CREDENTIAL_URL = "http://localhost:8090";
|
|
40
|
+
process.env.CES_SERVICE_TOKEN = "test-token";
|
|
41
|
+
const mockFetch = mock(async () => {
|
|
42
|
+
return new Response(JSON.stringify({ value: "http-secret" }), {
|
|
43
|
+
status: 200,
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
});
|
|
46
|
+
}) as unknown as typeof fetch;
|
|
47
|
+
mockFetch.preconnect = originalFetch.preconnect;
|
|
48
|
+
globalThis.fetch = mockFetch;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
globalThis.fetch = originalFetch;
|
|
53
|
+
delete process.env.IS_CONTAINERIZED;
|
|
54
|
+
delete process.env.CES_CREDENTIAL_URL;
|
|
55
|
+
delete process.env.CES_SERVICE_TOKEN;
|
|
56
|
+
_resetBackend();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("falls back from dead CES RPC transport to CES HTTP in managed mode", async () => {
|
|
60
|
+
setCesClient(createMockCesClient());
|
|
61
|
+
|
|
62
|
+
expect(await getSecureKeyAsync("openai")).toBeUndefined();
|
|
63
|
+
expect(getActiveBackendName()).toBe("ces-rpc");
|
|
64
|
+
expect(rpcCall).toHaveBeenCalledTimes(1);
|
|
65
|
+
|
|
66
|
+
rpcReady = false;
|
|
67
|
+
|
|
68
|
+
expect(await getSecureKeyAsync("openai")).toBe("http-secret");
|
|
69
|
+
expect(getActiveBackendName()).toBe("ces-http");
|
|
70
|
+
expect(rpcCall).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -19,6 +19,7 @@ afterEach(() => {
|
|
|
19
19
|
const DECLARED_FLAG_ID = "contacts";
|
|
20
20
|
const DECLARED_FLAG_KEY = DECLARED_FLAG_ID;
|
|
21
21
|
const DECLARED_SKILL_ID = "contacts";
|
|
22
|
+
const APP_BUILDER_MULTIFILE_FLAG_KEY = "app-builder-multifile";
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Helpers
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -158,6 +159,13 @@ describe("isAssistantFeatureFlagEnabled", () => {
|
|
|
158
159
|
// browser is declared in the registry with defaultEnabled: true
|
|
159
160
|
expect(isAssistantFeatureFlagEnabled("browser", config)).toBe(true);
|
|
160
161
|
});
|
|
162
|
+
|
|
163
|
+
test("app-builder-multifile defaults to enabled when no override is set", () => {
|
|
164
|
+
const config = makeConfig();
|
|
165
|
+
expect(
|
|
166
|
+
isAssistantFeatureFlagEnabled(APP_BUILDER_MULTIFILE_FLAG_KEY, config),
|
|
167
|
+
).toBe(true);
|
|
168
|
+
});
|
|
161
169
|
});
|
|
162
170
|
|
|
163
171
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Guard test: SKILL.md files must never instruct the assistant to accept
|
|
6
|
+
* secrets (passwords, API keys, tokens, etc.) pasted directly in chat.
|
|
7
|
+
*
|
|
8
|
+
* Secrets must always be collected via `credential_store prompt`, which
|
|
9
|
+
* presents a secure native UI that keeps the value out of conversation
|
|
10
|
+
* history and LLM context.
|
|
11
|
+
*
|
|
12
|
+
* This guard prevents regressions like the gmail/messaging bundled skill
|
|
13
|
+
* violation where SKILL.md contained "Include client_secret too if they
|
|
14
|
+
* provide one" — directing the assistant to accept a secret value from
|
|
15
|
+
* the chat stream.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** SKILL.md files permitted to contain otherwise-violating patterns. */
|
|
19
|
+
const ALLOWLIST = new Set<string>([
|
|
20
|
+
// Add paths here only if there is a genuine, documented exception.
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Words that indicate the line is about a secret/credential value.
|
|
25
|
+
*/
|
|
26
|
+
const SECRET_WORDS =
|
|
27
|
+
"secret|password|api[_\\s-]?key|auth[_\\s-]?token|private[_\\s-]?key|access[_\\s-]?token|client[_\\s-]?secret|signing[_\\s-]?key|bearer[_\\s-]?token";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Patterns that indicate the assistant is being told to accept a secret
|
|
31
|
+
* value directly in chat, rather than via credential_store prompt.
|
|
32
|
+
*/
|
|
33
|
+
const VIOLATION_PATTERNS: RegExp[] = [
|
|
34
|
+
// "accept <secret> in/via/from chat/plaintext/the conversation"
|
|
35
|
+
new RegExp(
|
|
36
|
+
`accept\\s+.*(?:${SECRET_WORDS}).*\\b(?:in|via|from)\\s+(?:chat|plaintext|the\\s+conversation)`,
|
|
37
|
+
"i",
|
|
38
|
+
),
|
|
39
|
+
new RegExp(
|
|
40
|
+
`accept\\s+.*\\b(?:in|via|from)\\s+(?:chat|plaintext|the\\s+conversation).*(?:${SECRET_WORDS})`,
|
|
41
|
+
"i",
|
|
42
|
+
),
|
|
43
|
+
// "ask (the user|them) (for|to share/send/paste/type/provide) <secret>" where destination is chat
|
|
44
|
+
// Must have "the user" or "them" as the object to avoid matching third-party descriptions
|
|
45
|
+
// like "Discord will ask for a 2FA code before revealing the secret"
|
|
46
|
+
new RegExp(
|
|
47
|
+
`ask\\s+(?:the\\s+user|them)\\s+(?:for|to\\s+(?:share|send|paste|type|provide))\\s+(?:the\\s+|their\\s+|a\\s+)?(?:${SECRET_WORDS})`,
|
|
48
|
+
"i",
|
|
49
|
+
),
|
|
50
|
+
// "Include <secret> too if they provide one" — the original gmail violation pattern
|
|
51
|
+
new RegExp(
|
|
52
|
+
`include\\s+(?:the\\s+)?(?:${SECRET_WORDS})\\s+(?:too|as\\s+well|also)\\s+if\\s+they\\s+provide`,
|
|
53
|
+
"i",
|
|
54
|
+
),
|
|
55
|
+
// "<secret> pasted/typed/sent in chat/conversation/plaintext"
|
|
56
|
+
new RegExp(
|
|
57
|
+
`(?:${SECRET_WORDS})\\s+(?:pasted|typed|sent|provided|shared)\\s+(?:in|via|through)\\s+(?:chat|conversation|plaintext)`,
|
|
58
|
+
"i",
|
|
59
|
+
),
|
|
60
|
+
// "paste/type/send <secret> in chat/here/the conversation"
|
|
61
|
+
new RegExp(
|
|
62
|
+
`(?:paste|type|send|share|provide)\\s+(?:the\\s+|your\\s+|their\\s+)?(?:${SECRET_WORDS})\\s+(?:in\\s+(?:chat|the\\s+conversation)|here)`,
|
|
63
|
+
"i",
|
|
64
|
+
),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lines containing these negation words are typically instructing the
|
|
69
|
+
* assistant NOT to do something — these are not violations.
|
|
70
|
+
*/
|
|
71
|
+
const NEGATION_PATTERNS =
|
|
72
|
+
/\b(?:never|do\s+not|don['']t|must\s+not|should\s+not|shouldn['']t)\b|\bNOT\b/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lines that are YAML-style field values within a credential_store prompt
|
|
76
|
+
* block (label, description, placeholder). These contain secret-related
|
|
77
|
+
* words but are secure UI text, not chat instructions.
|
|
78
|
+
*/
|
|
79
|
+
const CREDENTIAL_STORE_UI_FIELD =
|
|
80
|
+
/^\s*(?:[-*]\s+)?(?:label|description|placeholder)\s*[:=]\s*/i;
|
|
81
|
+
|
|
82
|
+
interface Violation {
|
|
83
|
+
file: string;
|
|
84
|
+
line: number;
|
|
85
|
+
text: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findViolations(): Violation[] {
|
|
89
|
+
const repoRoot = process.cwd() + "/..";
|
|
90
|
+
|
|
91
|
+
// Find all SKILL.md files tracked by git
|
|
92
|
+
let skillFiles: string[];
|
|
93
|
+
try {
|
|
94
|
+
const output = execSync(`git grep -l "" -- '*/SKILL.md'`, {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
cwd: repoRoot,
|
|
97
|
+
}).trim();
|
|
98
|
+
skillFiles = output.split("\n").filter((f) => f.length > 0);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if ((err as { status?: number }).status === 1) {
|
|
101
|
+
return []; // no SKILL.md files found
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Filter to skills/ and assistant/src/config/bundled-skills/ directories
|
|
107
|
+
skillFiles = skillFiles.filter(
|
|
108
|
+
(f) =>
|
|
109
|
+
f.startsWith("skills/") ||
|
|
110
|
+
f.startsWith("assistant/src/config/bundled-skills/"),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const violations: Violation[] = [];
|
|
114
|
+
|
|
115
|
+
for (const filePath of skillFiles) {
|
|
116
|
+
if (ALLOWLIST.has(filePath)) continue;
|
|
117
|
+
|
|
118
|
+
let content: string;
|
|
119
|
+
try {
|
|
120
|
+
content = execSync(`git show HEAD:${filePath}`, {
|
|
121
|
+
encoding: "utf-8",
|
|
122
|
+
cwd: repoRoot,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lines = content.split("\n");
|
|
129
|
+
|
|
130
|
+
// Track whether we're inside a credential_store prompt block
|
|
131
|
+
// (indented YAML-like content after a credential_store mention)
|
|
132
|
+
let inCredentialStoreBlock = false;
|
|
133
|
+
let blockIndent = 0;
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
const line = lines[i];
|
|
137
|
+
const lineNumber = i + 1;
|
|
138
|
+
|
|
139
|
+
// Track credential_store prompt blocks
|
|
140
|
+
if (/credential_store\s+prompt/i.test(line)) {
|
|
141
|
+
inCredentialStoreBlock = true;
|
|
142
|
+
blockIndent = line.search(/\S/);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Exit credential_store block when indentation returns to same or lesser level
|
|
147
|
+
if (inCredentialStoreBlock) {
|
|
148
|
+
const currentIndent = line.search(/\S/);
|
|
149
|
+
if (
|
|
150
|
+
currentIndent !== -1 &&
|
|
151
|
+
currentIndent <= blockIndent &&
|
|
152
|
+
line.trim().length > 0
|
|
153
|
+
) {
|
|
154
|
+
inCredentialStoreBlock = false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Skip empty lines
|
|
159
|
+
if (line.trim().length === 0) continue;
|
|
160
|
+
|
|
161
|
+
// Skip negation lines — these instruct NOT to do something
|
|
162
|
+
if (NEGATION_PATTERNS.test(line)) continue;
|
|
163
|
+
|
|
164
|
+
// Skip credential_store UI field lines (label:, description:, placeholder:)
|
|
165
|
+
if (inCredentialStoreBlock && CREDENTIAL_STORE_UI_FIELD.test(line))
|
|
166
|
+
continue;
|
|
167
|
+
|
|
168
|
+
// Strip markdown backticks before pattern matching so that
|
|
169
|
+
// violations like `client_secret` are caught the same as bare words.
|
|
170
|
+
const stripped = line.replace(/`/g, "");
|
|
171
|
+
|
|
172
|
+
// Check against violation patterns
|
|
173
|
+
for (const pattern of VIOLATION_PATTERNS) {
|
|
174
|
+
if (pattern.test(stripped)) {
|
|
175
|
+
violations.push({
|
|
176
|
+
file: filePath,
|
|
177
|
+
line: lineNumber,
|
|
178
|
+
text: line.trim(),
|
|
179
|
+
});
|
|
180
|
+
break; // one violation per line is enough
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return violations;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
describe("SKILL.md secret handling guard", () => {
|
|
190
|
+
test("no SKILL.md files instruct accepting secrets in chat", () => {
|
|
191
|
+
const violations = findViolations();
|
|
192
|
+
|
|
193
|
+
if (violations.length > 0) {
|
|
194
|
+
const message = [
|
|
195
|
+
"Found SKILL.md files that instruct accepting secrets directly in chat.",
|
|
196
|
+
"Secrets must always be collected via `credential_store prompt`, which",
|
|
197
|
+
"presents a secure native UI that keeps values out of conversation history.",
|
|
198
|
+
"",
|
|
199
|
+
"Violations:",
|
|
200
|
+
...violations.map((v) => ` - ${v.file}:${v.line}: ${v.text}`),
|
|
201
|
+
"",
|
|
202
|
+
"To fix: replace chat-based secret collection with a `credential_store prompt` call.",
|
|
203
|
+
"See any *-setup skill (e.g. skills/slack-app-setup/SKILL.md) for the correct pattern.",
|
|
204
|
+
"",
|
|
205
|
+
"If this is a genuine exception, add the file path to the ALLOWLIST in",
|
|
206
|
+
"skill-secret-handling-guard.test.ts.",
|
|
207
|
+
].join("\n");
|
|
208
|
+
|
|
209
|
+
expect(violations, message).toEqual([]);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -58,7 +58,7 @@ describe("assistant skills uninstall", () => {
|
|
|
58
58
|
|
|
59
59
|
// GIVEN a skill is installed locally
|
|
60
60
|
installFakeSkill("weather");
|
|
61
|
-
writeSkillsIndex("- weather\n- google-oauth-
|
|
61
|
+
writeSkillsIndex("- weather\n- google-oauth-app-setup\n");
|
|
62
62
|
|
|
63
63
|
// WHEN we uninstall the skill
|
|
64
64
|
uninstallSkillLocally("weather");
|
|
@@ -71,7 +71,7 @@ describe("assistant skills uninstall", () => {
|
|
|
71
71
|
expect(index).not.toContain("weather");
|
|
72
72
|
|
|
73
73
|
// AND other skills should remain in the index
|
|
74
|
-
expect(index).toContain("google-oauth-
|
|
74
|
+
expect(index).toContain("google-oauth-app-setup");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
test("errors when skill is not installed", () => {
|