@vellumai/assistant 0.8.12-staging.1 → 0.8.12

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 (35) hide show
  1. package/openapi.yaml +507 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  4. package/src/__tests__/log-export-workspace.test.ts +468 -3
  5. package/src/__tests__/secret-fixtures.ts +20 -0
  6. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  7. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  8. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  9. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  10. package/src/acp/__tests__/agent-process.test.ts +315 -2
  11. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  12. package/src/acp/agent-process.ts +163 -34
  13. package/src/acp/prepare-agent-env.ts +55 -15
  14. package/src/bundler/app-compiler.ts +8 -0
  15. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +10 -4
  16. package/src/cli/lib/upgrade-plugin.ts +13 -7
  17. package/src/config/seed-inference-profiles.ts +4 -8
  18. package/src/events/tool-audit-listener.ts +40 -9
  19. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  20. package/src/providers/model-catalog.ts +28 -0
  21. package/src/providers/model-intents.ts +1 -1
  22. package/src/providers/openai/chat-completions-provider.ts +2 -1
  23. package/src/providers/openai/responses-provider.ts +2 -1
  24. package/src/providers/unparseable-tool-args.ts +56 -0
  25. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  26. package/src/runtime/routes/__tests__/plugins-routes.test.ts +347 -0
  27. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  28. package/src/runtime/routes/log-export-routes.ts +143 -96
  29. package/src/runtime/routes/plugins-routes.ts +359 -0
  30. package/src/runtime/routes/redact-staged-export.ts +259 -0
  31. package/src/security/redact-json.ts +61 -0
  32. package/src/tools/tool-approval-handler.ts +31 -0
  33. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  34. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  35. package/src/workspace/migrations/registry.ts +4 -0
@@ -10,6 +10,10 @@ mock.module("../config/loader.js", () => ({
10
10
 
11
11
  import { createToolAuditListener } from "../events/tool-audit-listener.js";
12
12
  import type { ToolInvocationRecord } from "../memory/tool-usage-store.js";
13
+ import {
14
+ OPENAI_PROJECT_KEY_REDACTION_MARKER,
15
+ SYNTHETIC_OPENAI_PROJECT_KEY,
16
+ } from "./secret-fixtures.js";
13
17
 
14
18
  describe("tool audit listener", () => {
15
19
  beforeEach(() => {
@@ -101,6 +105,88 @@ describe("tool audit listener", () => {
101
105
  expect(records[0].result).toContain("<redacted");
102
106
  });
103
107
 
108
+ test("redacts known-pattern secrets in tool inputs before recording", () => {
109
+ const records: ToolInvocationRecord[] = [];
110
+ const listener = createToolAuditListener((record) => records.push(record));
111
+
112
+ const input = {
113
+ command: `export OPENAI_API_KEY="${SYNTHETIC_OPENAI_PROJECT_KEY}"`,
114
+ };
115
+
116
+ listener({
117
+ type: "executed",
118
+ toolName: "bash",
119
+ input,
120
+ workingDir: "/tmp",
121
+ conversationId: "conv-input-redact",
122
+ riskLevel: "low",
123
+ decision: "allow",
124
+ durationMs: 5,
125
+ result: { content: "ok", isError: false },
126
+ });
127
+ listener({
128
+ type: "error",
129
+ toolName: "bash",
130
+ input,
131
+ workingDir: "/tmp",
132
+ conversationId: "conv-input-redact",
133
+ riskLevel: "low",
134
+ decision: "error",
135
+ durationMs: 5,
136
+ errorMessage: "boom",
137
+ isExpected: false,
138
+ errorCategory: "tool_failure",
139
+ });
140
+ listener({
141
+ type: "permission_denied",
142
+ toolName: "bash",
143
+ input,
144
+ workingDir: "/tmp",
145
+ conversationId: "conv-input-redact",
146
+ riskLevel: "high",
147
+ decision: "deny",
148
+ reason: "Permission denied by user",
149
+ durationMs: 5,
150
+ });
151
+
152
+ expect(records).toHaveLength(3);
153
+ for (const record of records) {
154
+ expect(record.input).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
155
+ // The stored input must remain parseable JSON: string leaves are
156
+ // redacted before stringification, so the marker lands inside a JSON
157
+ // string value (its quotes escaped) instead of corrupting the
158
+ // serialized form.
159
+ const parsed = JSON.parse(record.input) as { command: string };
160
+ expect(parsed.command).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
161
+ }
162
+ });
163
+
164
+ test("benign inputs round-trip byte-identical (no false-positive mangling)", () => {
165
+ const records: ToolInvocationRecord[] = [];
166
+ const listener = createToolAuditListener((record) => records.push(record));
167
+
168
+ const input = {
169
+ command: "git log --oneline -5 && echo done",
170
+ cwd: "/tmp/project",
171
+ env: { LANG: "en_US.UTF-8" },
172
+ };
173
+
174
+ listener({
175
+ type: "executed",
176
+ toolName: "bash",
177
+ input,
178
+ workingDir: "/tmp",
179
+ conversationId: "conv-benign-input",
180
+ riskLevel: "low",
181
+ decision: "allow",
182
+ durationMs: 2,
183
+ result: { content: "ok", isError: false },
184
+ });
185
+
186
+ expect(records).toHaveLength(1);
187
+ expect(records[0].input).toBe(JSON.stringify(input));
188
+ });
189
+
104
190
  test("does not redact non-secret content like UUIDs or hashes", () => {
105
191
  const records: ToolInvocationRecord[] = [];
106
192
  const listener = createToolAuditListener((record) => records.push(record));
@@ -0,0 +1,174 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { upgradeQualityProfileToFable5Migration } from "../workspace/migrations/100-upgrade-quality-profile-to-fable-5.js";
13
+
14
+ let workspaceDir: string;
15
+
16
+ function writeConfig(data: Record<string, unknown>): void {
17
+ writeFileSync(
18
+ join(workspaceDir, "config.json"),
19
+ JSON.stringify(data, null, 2) + "\n",
20
+ );
21
+ }
22
+
23
+ function readConfig(): Record<string, unknown> {
24
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
25
+ }
26
+
27
+ function readProfiles(): Record<string, Record<string, unknown>> {
28
+ const llm = readConfig().llm as Record<string, unknown>;
29
+ return llm.profiles as Record<string, Record<string, unknown>>;
30
+ }
31
+
32
+ beforeEach(() => {
33
+ workspaceDir = join(
34
+ tmpdir(),
35
+ `vellum-migration-100-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
36
+ );
37
+ mkdirSync(workspaceDir, { recursive: true });
38
+ });
39
+
40
+ afterEach(() => {
41
+ if (existsSync(workspaceDir)) {
42
+ rmSync(workspaceDir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ describe("100-upgrade-quality-profile-to-fable-5 migration", () => {
47
+ test("no-op when config.json does not exist", () => {
48
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
49
+ expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
50
+ });
51
+
52
+ test("no-op when config has no llm.profiles", () => {
53
+ const original = { llm: { default: { provider: "anthropic" } } };
54
+ writeConfig(original);
55
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
56
+ expect(readConfig()).toEqual(original);
57
+ });
58
+
59
+ test("gracefully handles invalid JSON", () => {
60
+ writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
61
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
62
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
63
+ "not-valid-json",
64
+ );
65
+ });
66
+
67
+ test("upgrades quality-optimized from claude-opus-4-7", () => {
68
+ writeConfig({
69
+ llm: {
70
+ profiles: {
71
+ "quality-optimized": {
72
+ provider: "anthropic",
73
+ model: "claude-opus-4-7",
74
+ label: "Quality",
75
+ },
76
+ },
77
+ },
78
+ });
79
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
80
+ const profile = readProfiles()["quality-optimized"]!;
81
+ expect(profile.model).toBe("claude-fable-5");
82
+ expect(profile.label).toBe("Quality");
83
+ });
84
+
85
+ test("upgrades the managed profile but not the user-owned custom copy", () => {
86
+ writeConfig({
87
+ llm: {
88
+ profiles: {
89
+ "quality-optimized": {
90
+ provider: "anthropic",
91
+ model: "claude-opus-4-8",
92
+ },
93
+ "custom-quality-optimized": {
94
+ provider: "anthropic",
95
+ model: "claude-opus-4-8",
96
+ },
97
+ },
98
+ },
99
+ });
100
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
101
+ const profiles = readProfiles();
102
+ expect(profiles["quality-optimized"]!.model).toBe("claude-fable-5");
103
+ expect(profiles["custom-quality-optimized"]!.model).toBe("claude-opus-4-8");
104
+ });
105
+
106
+ test("upgrades OpenRouter-prefixed model ids", () => {
107
+ writeConfig({
108
+ llm: {
109
+ profiles: {
110
+ "quality-optimized": {
111
+ provider: "openrouter",
112
+ model: "anthropic/claude-opus-4.8",
113
+ },
114
+ },
115
+ },
116
+ });
117
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
118
+ expect(readProfiles()["quality-optimized"]!.model).toBe(
119
+ "anthropic/claude-fable-5",
120
+ );
121
+ });
122
+
123
+ test("leaves user-customized models untouched", () => {
124
+ const original = {
125
+ llm: {
126
+ profiles: {
127
+ "quality-optimized": { provider: "openai", model: "gpt-5.4" },
128
+ },
129
+ },
130
+ };
131
+ writeConfig(original);
132
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
133
+ expect(readConfig()).toEqual(original);
134
+ });
135
+
136
+ test("leaves non-quality profiles untouched", () => {
137
+ writeConfig({
138
+ llm: {
139
+ profiles: {
140
+ balanced: { provider: "anthropic", model: "claude-opus-4-8" },
141
+ "quality-optimized": {
142
+ provider: "anthropic",
143
+ model: "claude-opus-4-8",
144
+ },
145
+ },
146
+ },
147
+ });
148
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
149
+ const profiles = readProfiles();
150
+ expect(profiles["balanced"]!.model).toBe("claude-opus-4-8");
151
+ expect(profiles["quality-optimized"]!.model).toBe("claude-fable-5");
152
+ });
153
+
154
+ test("idempotency: no-op on already-upgraded config (no writes)", () => {
155
+ writeConfig({
156
+ llm: {
157
+ profiles: {
158
+ "quality-optimized": {
159
+ provider: "anthropic",
160
+ model: "claude-fable-5",
161
+ },
162
+ },
163
+ },
164
+ });
165
+ const beforeContent = readFileSync(
166
+ join(workspaceDir, "config.json"),
167
+ "utf-8",
168
+ );
169
+ upgradeQualityProfileToFable5Migration.run(workspaceDir);
170
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
171
+ beforeContent,
172
+ );
173
+ });
174
+ });
@@ -0,0 +1,162 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { upgradeBalancedEconomyToMinimaxM3Migration } from "../workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.js";
13
+
14
+ let workspaceDir: string;
15
+
16
+ function writeConfig(data: Record<string, unknown>): void {
17
+ writeFileSync(
18
+ join(workspaceDir, "config.json"),
19
+ JSON.stringify(data, null, 2) + "\n",
20
+ );
21
+ }
22
+
23
+ function readConfig(): Record<string, unknown> {
24
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
25
+ }
26
+
27
+ function readProfiles(): Record<string, Record<string, unknown>> {
28
+ const llm = readConfig().llm as Record<string, unknown>;
29
+ return llm.profiles as Record<string, Record<string, unknown>>;
30
+ }
31
+
32
+ beforeEach(() => {
33
+ workspaceDir = join(
34
+ tmpdir(),
35
+ `vellum-migration-101-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
36
+ );
37
+ mkdirSync(workspaceDir, { recursive: true });
38
+ });
39
+
40
+ afterEach(() => {
41
+ if (existsSync(workspaceDir)) {
42
+ rmSync(workspaceDir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ describe("101-upgrade-balanced-economy-to-minimax-m3 migration", () => {
47
+ test("no-op when config.json does not exist", () => {
48
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
49
+ expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
50
+ });
51
+
52
+ test("no-op when config has no llm.profiles", () => {
53
+ const original = { llm: { default: { provider: "fireworks" } } };
54
+ writeConfig(original);
55
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
56
+ expect(readConfig()).toEqual(original);
57
+ });
58
+
59
+ test("gracefully handles invalid JSON", () => {
60
+ writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
61
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
62
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
63
+ "not-valid-json",
64
+ );
65
+ });
66
+
67
+ test("upgrades balanced-economy from Kimi K2.6 and drops the logit-bias preset", () => {
68
+ writeConfig({
69
+ llm: {
70
+ profiles: {
71
+ "balanced-economy": {
72
+ provider: "fireworks",
73
+ provider_connection: "fireworks-managed",
74
+ model: "accounts/fireworks/models/kimi-k2p6",
75
+ label: "Balanced Economy",
76
+ description: "Strong open model (Kimi K2.6) at a lower price point",
77
+ maxTokens: 16000,
78
+ effort: "high",
79
+ thinking: { enabled: true, streamThinking: true },
80
+ contextWindow: { maxInputTokens: 200000 },
81
+ logitBias: "suppress-cjk",
82
+ },
83
+ },
84
+ },
85
+ });
86
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
87
+ const profile = readProfiles()["balanced-economy"]!;
88
+ expect(profile.model).toBe("accounts/fireworks/models/minimax-m3");
89
+ expect(profile.description).toBe(
90
+ "Strong open model (MiniMax M3) at a lower price point",
91
+ );
92
+ expect(profile.maxTokens).toBe(32000);
93
+ expect(profile.contextWindow).toEqual({ maxInputTokens: 200000 });
94
+ expect("logitBias" in profile).toBe(false);
95
+ expect(profile.label).toBe("Balanced Economy");
96
+ expect(profile.effort).toBe("high");
97
+ expect(profile.thinking).toEqual({ enabled: true, streamThinking: true });
98
+ });
99
+
100
+ test("leaves user-customized models untouched", () => {
101
+ const original = {
102
+ llm: {
103
+ profiles: {
104
+ "balanced-economy": {
105
+ provider: "fireworks",
106
+ model: "accounts/fireworks/models/deepseek-v4-pro",
107
+ logitBias: "suppress-cjk",
108
+ },
109
+ },
110
+ },
111
+ };
112
+ writeConfig(original);
113
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
114
+ expect(readConfig()).toEqual(original);
115
+ });
116
+
117
+ test("leaves other profiles untouched", () => {
118
+ writeConfig({
119
+ llm: {
120
+ profiles: {
121
+ "my-kimi": {
122
+ provider: "fireworks",
123
+ model: "accounts/fireworks/models/kimi-k2p6",
124
+ },
125
+ "balanced-economy": {
126
+ provider: "fireworks",
127
+ model: "accounts/fireworks/models/kimi-k2p6",
128
+ },
129
+ },
130
+ },
131
+ });
132
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
133
+ const profiles = readProfiles();
134
+ expect(profiles["my-kimi"]!.model).toBe(
135
+ "accounts/fireworks/models/kimi-k2p6",
136
+ );
137
+ expect(profiles["balanced-economy"]!.model).toBe(
138
+ "accounts/fireworks/models/minimax-m3",
139
+ );
140
+ });
141
+
142
+ test("idempotency: no-op on already-upgraded config (no writes)", () => {
143
+ writeConfig({
144
+ llm: {
145
+ profiles: {
146
+ "balanced-economy": {
147
+ provider: "fireworks",
148
+ model: "accounts/fireworks/models/minimax-m3",
149
+ },
150
+ },
151
+ },
152
+ });
153
+ const beforeContent = readFileSync(
154
+ join(workspaceDir, "config.json"),
155
+ "utf-8",
156
+ );
157
+ upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
158
+ expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
159
+ beforeContent,
160
+ );
161
+ });
162
+ });