@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
@@ -0,0 +1,61 @@
1
+ /**
2
+ * JSON-aware secret redaction: walk a parsed JSON value and run
3
+ * `redactSecrets()` over every string leaf (object keys included).
4
+ *
5
+ * The redaction marker (`<redacted type="..." />`) contains double quotes,
6
+ * so splicing it into an already-serialized JSON document corrupts any
7
+ * string it lands inside. Callers that need to keep serialized JSON valid
8
+ * must redact the string leaves of the parsed value and re-stringify,
9
+ * rather than redacting the serialized text.
10
+ *
11
+ * Shared by the export-time staged-file sweep (`redact-staged-export.ts`)
12
+ * and the tool-audit listener (`tool-audit-listener.ts`). The hot-path pino
13
+ * log serializer (`util/log-redact.ts`) intentionally does not use this
14
+ * module.
15
+ */
16
+
17
+ import { redactSecrets } from "./secret-scanner.js";
18
+
19
+ export interface RedactedJsonValue {
20
+ /** Redacted copy of the input (the input itself is never mutated). */
21
+ value: unknown;
22
+ /** True when at least one string leaf was changed by redaction. */
23
+ changed: boolean;
24
+ }
25
+
26
+ interface RedactionState {
27
+ changed: boolean;
28
+ }
29
+
30
+ /**
31
+ * Redact secrets in every string leaf (keys included) of a parsed JSON
32
+ * value. Returns the redacted copy plus a `changed` flag so callers can
33
+ * keep unchanged content byte-identical (no gratuitous re-serialization).
34
+ */
35
+ export function redactJsonStringLeaves(value: unknown): RedactedJsonValue {
36
+ const state: RedactionState = { changed: false };
37
+ return { value: walk(value, state), changed: state.changed };
38
+ }
39
+
40
+ function walk(value: unknown, state: RedactionState): unknown {
41
+ if (typeof value === "string") {
42
+ return redactString(value, state);
43
+ }
44
+ if (Array.isArray(value)) {
45
+ return value.map((item) => walk(item, state));
46
+ }
47
+ if (value !== null && typeof value === "object") {
48
+ return Object.fromEntries(
49
+ Object.entries(value).map(
50
+ ([key, val]) => [redactString(key, state), walk(val, state)] as const,
51
+ ),
52
+ );
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function redactString(value: string, state: RedactionState): string {
58
+ const redacted = redactSecrets(value);
59
+ if (redacted !== value) state.changed = true;
60
+ return redacted;
61
+ }
@@ -5,6 +5,10 @@ import {
5
5
  getCanonicalGuardianRequest,
6
6
  updateCanonicalGuardianRequest,
7
7
  } from "../memory/canonical-guardian-store.js";
8
+ import {
9
+ isUnparseableToolArgs,
10
+ unparseableToolArgsMessage,
11
+ } from "../providers/unparseable-tool-args.js";
8
12
  import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
9
13
  import { createOrReuseToolGrantRequest } from "../runtime/tool-grant-request-helper.js";
10
14
  import { redactSecrets } from "../security/secret-scanner.js";
@@ -241,6 +245,33 @@ export class ToolApprovalHandler {
241
245
  };
242
246
  }
243
247
 
248
+ // Reject tool calls whose arguments failed JSON parsing in the provider
249
+ // layer (wrapped under the `_raw` marker). Executing with the marker
250
+ // object would feed garbage input to the tool — and worse, a tool that
251
+ // tolerates missing fields can "succeed" (e.g. ui_show creating a
252
+ // typeless surface), so the model never learns its arguments were
253
+ // mangled. Fail loudly instead so the model retries.
254
+ if (isUnparseableToolArgs(input)) {
255
+ const msg = unparseableToolArgsMessage(name, input._raw);
256
+ const durationMs = Date.now() - startTime;
257
+ emitLifecycleEvent({
258
+ type: "error",
259
+ toolName: name,
260
+ executionTarget,
261
+ input,
262
+ workingDir: context.workingDir,
263
+ conversationId: context.conversationId,
264
+ requestId: context.requestId,
265
+ riskLevel,
266
+ decision: "error",
267
+ durationMs,
268
+ errorMessage: msg,
269
+ isExpected: true,
270
+ errorCategory: "tool_failure",
271
+ });
272
+ return { allowed: false, result: { content: msg, isError: true } };
273
+ }
274
+
244
275
  // Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
245
276
  const guardianCheck = enforceVerificationControlPlanePolicy(
246
277
  name,
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ // Upgrade quality-optimized profiles to Claude Fable 5.
7
+ //
8
+ // The quality-optimized model intent moved to claude-fable-5 (#34498), but
9
+ // the intent map is only consulted when a profile is seeded: off-platform
10
+ // installs reseed managed profiles on every boot, while on-platform
11
+ // workspaces keep the profile they were hatched with — the seeder skips
12
+ // existing profiles there, and the hatch overlay is consumed exactly once.
13
+ // Existing platform assistants are therefore stuck on whichever model the
14
+ // quality intent pointed at when they hatched (claude-opus-4-7 or
15
+ // claude-opus-4-8) until a migration rewrites it.
16
+ //
17
+ // Only the managed quality-optimized profile is touched — the user-owned
18
+ // custom-quality-optimized copy is theirs to manage. Within it, only a model
19
+ // still on a previous quality-intent default is upgraded — matching by model
20
+ // value also covers the OpenRouter-prefixed ids. A profile whose model the
21
+ // user changed to anything else is left untouched.
22
+
23
+ const TARGET_PROFILES = ["quality-optimized"];
24
+
25
+ const MODEL_UPGRADES: Record<string, string> = {
26
+ "claude-opus-4-7": "claude-fable-5",
27
+ "claude-opus-4-8": "claude-fable-5",
28
+ "anthropic/claude-opus-4.7": "anthropic/claude-fable-5",
29
+ "anthropic/claude-opus-4.8": "anthropic/claude-fable-5",
30
+ };
31
+
32
+ export const upgradeQualityProfileToFable5Migration: WorkspaceMigration = {
33
+ id: "100-upgrade-quality-profile-to-fable-5",
34
+ description:
35
+ "Upgrade the managed quality-optimized profile from Opus 4.7/4.8 to Claude Fable 5",
36
+ run(workspaceDir: string): void {
37
+ const configPath = join(workspaceDir, "config.json");
38
+ if (!existsSync(configPath)) return;
39
+
40
+ let config: Record<string, unknown>;
41
+ try {
42
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
43
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
44
+ config = raw as Record<string, unknown>;
45
+ } catch {
46
+ return;
47
+ }
48
+
49
+ const llm = readObject(config.llm);
50
+ if (llm === null) return;
51
+
52
+ const profiles = readObject(llm.profiles);
53
+ if (profiles === null) return;
54
+
55
+ let changed = false;
56
+
57
+ for (const name of TARGET_PROFILES) {
58
+ const profile = readObject(profiles[name]);
59
+ if (profile === null) continue;
60
+ if (typeof profile.model !== "string") continue;
61
+
62
+ const upgraded = MODEL_UPGRADES[profile.model];
63
+ if (upgraded === undefined) continue;
64
+
65
+ profile.model = upgraded;
66
+ profiles[name] = profile;
67
+ changed = true;
68
+ }
69
+
70
+ if (changed) {
71
+ llm.profiles = profiles;
72
+ config.llm = llm;
73
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
74
+ }
75
+ },
76
+ down(_workspaceDir: string): void {
77
+ // Forward-only.
78
+ },
79
+ };
80
+
81
+ function readObject(value: unknown): Record<string, unknown> | null {
82
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
83
+ return null;
84
+ }
85
+ return value as Record<string, unknown>;
86
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ // Switch the Balanced Economy profile from Kimi K2.6 to MiniMax M3.
7
+ //
8
+ // The Fireworks balanced model intent moved to MiniMax M3 (#34726), but the
9
+ // intent map is only consulted when a profile is seeded: off-platform
10
+ // installs reseed managed profiles on every boot, while on-platform
11
+ // workspaces keep the profile they were hatched with — the seeder skips
12
+ // existing profiles there. Existing platform assistants would otherwise stay
13
+ // on Kimi K2.6 (including the now-dropped `suppress-cjk` logit-bias preset)
14
+ // until a migration rewrites the profile.
15
+ //
16
+ // Only the managed balanced-economy profile still pointing at the old
17
+ // default model is touched — a profile whose model the user changed is left
18
+ // alone. The rewrite mirrors the new seed template: MiniMax M3, 32K max
19
+ // output tokens, no logit-bias preset.
20
+
21
+ const OLD_MODEL = "accounts/fireworks/models/kimi-k2p6";
22
+ const NEW_MODEL = "accounts/fireworks/models/minimax-m3";
23
+
24
+ export const upgradeBalancedEconomyToMinimaxM3Migration: WorkspaceMigration = {
25
+ id: "101-upgrade-balanced-economy-to-minimax-m3",
26
+ description:
27
+ "Switch the managed Balanced Economy profile from Kimi K2.6 to MiniMax M3",
28
+ run(workspaceDir: string): void {
29
+ const configPath = join(workspaceDir, "config.json");
30
+ if (!existsSync(configPath)) return;
31
+
32
+ let config: Record<string, unknown>;
33
+ try {
34
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
35
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
36
+ config = raw as Record<string, unknown>;
37
+ } catch {
38
+ return;
39
+ }
40
+
41
+ const llm = readObject(config.llm);
42
+ if (llm === null) return;
43
+
44
+ const profiles = readObject(llm.profiles);
45
+ if (profiles === null) return;
46
+
47
+ const profile = readObject(profiles["balanced-economy"]);
48
+ if (profile === null || profile.model !== OLD_MODEL) return;
49
+
50
+ profile.model = NEW_MODEL;
51
+ profile.description =
52
+ "Strong open model (MiniMax M3) at a lower price point";
53
+ profile.maxTokens = 32000;
54
+ delete profile.logitBias;
55
+ profiles["balanced-economy"] = profile;
56
+ llm.profiles = profiles;
57
+ config.llm = llm;
58
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
59
+ },
60
+ down(_workspaceDir: string): void {
61
+ // Forward-only.
62
+ },
63
+ };
64
+
65
+ function readObject(value: unknown): Record<string, unknown> | null {
66
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
67
+ return null;
68
+ }
69
+ return value as Record<string, unknown>;
70
+ }
@@ -97,6 +97,8 @@ import { reduceQualityProfileEffortMigration } from "./096-reduce-quality-profil
97
97
  import { enableAdaptiveThinkingManagedProfilesMigration } from "./097-enable-adaptive-thinking-managed-profiles.js";
98
98
  import { removeStaleUpdatesBulletinFileMigration } from "./098-remove-stale-updates-bulletin-file.js";
99
99
  import { disableCacheOneShotCallsitesMigration } from "./099-disable-cache-one-shot-callsites.js";
100
+ import { upgradeQualityProfileToFable5Migration } from "./100-upgrade-quality-profile-to-fable-5.js";
101
+ import { upgradeBalancedEconomyToMinimaxM3Migration } from "./101-upgrade-balanced-economy-to-minimax-m3.js";
100
102
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
101
103
  import type { WorkspaceMigration } from "./types.js";
102
104
 
@@ -205,4 +207,6 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
205
207
  enableAdaptiveThinkingManagedProfilesMigration,
206
208
  removeStaleUpdatesBulletinFileMigration,
207
209
  disableCacheOneShotCallsitesMigration,
210
+ upgradeQualityProfileToFable5Migration,
211
+ upgradeBalancedEconomyToMinimaxM3Migration,
208
212
  ];