@vellumai/cli 0.12.1 → 0.12.2-dev.202609162214.308bda0

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/src/lib/local.ts CHANGED
@@ -606,6 +606,24 @@ type DaemonStartOptions = {
606
606
  signingKey?: string;
607
607
  };
608
608
 
609
+ /**
610
+ * Directory that holds the local CES Unix socket (`ces.sock`).
611
+ *
612
+ * Matches CES's local data root: `{CREDENTIAL_SECURITY_DIR}/credential-executor`.
613
+ * Local CLI and managed sidecars share `CES_BOOTSTRAP_SOCKET_DIR`; this is the
614
+ * writable per-instance directory used outside containers.
615
+ */
616
+ function resolveCesBootstrapSocketDir(
617
+ resources?: LocalInstanceResources,
618
+ env?: Record<string, string | undefined>,
619
+ ): string {
620
+ const securityDir = resources
621
+ ? join(resources.instanceDir, ".vellum", "protected")
622
+ : env?.CREDENTIAL_SECURITY_DIR?.trim() ||
623
+ join(homedir(), ".vellum", "protected");
624
+ return join(securityDir, "credential-executor");
625
+ }
626
+
609
627
  /**
610
628
  * Apply per-instance resource overrides and shared daemon options to an
611
629
  * environment object. Called from all daemon spawn paths (source, watch,
@@ -644,11 +662,10 @@ function applyDaemonEnvOverrides(
644
662
  env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
645
663
  options.defaultWorkspaceConfigPath;
646
664
  }
647
- // Pin the daemon to the exact socket the sibling binds so the two agree
648
- // regardless of any stale CES_LOCAL_SOCKET inherited from the parent
649
- // environment. The assistant connects to the sibling instead of spawning
650
- // its own CES.
651
- env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
665
+ // CES and the assistant resolve the socket from CES_BOOTSTRAP_SOCKET_DIR.
666
+ const bootstrapDir = resolveCesBootstrapSocketDir(resources, env);
667
+ env.CES_BOOTSTRAP_SOCKET_DIR = bootstrapDir;
668
+ mkdirSync(bootstrapDir, { recursive: true });
652
669
  applyIpcSocketDirOverride(env);
653
670
  }
654
671
 
@@ -1022,26 +1039,18 @@ function resolveCesDir(resources?: LocalInstanceResources): string {
1022
1039
 
1023
1040
  /**
1024
1041
  * Resolve the local IPC endpoint shared by the CLI-launched CES sibling and
1025
- * assistant. Windows uses a named pipe. POSIX uses a Unix socket, including
1026
- * the existing short macOS fallback.
1042
+ * assistant. CES binds this path from `CES_BOOTSTRAP_SOCKET_DIR` via the same
1043
+ * `resolveIpcEndpoint("ces")` helper. Windows uses a named pipe. POSIX uses
1044
+ * a Unix socket, including the shared macOS AF_UNIX fallback.
1027
1045
  */
1028
1046
  export function resolveCesSocketPath(
1029
1047
  resources?: LocalInstanceResources,
1030
1048
  hostPlatform: NodeJS.Platform = platform(),
1031
1049
  ): string {
1032
- const workspaceDir = resources
1033
- ? join(resources.instanceDir, ".vellum", "workspace")
1034
- : join(homedir(), ".vellum", "workspace");
1035
- if (hostPlatform === "win32") {
1036
- return resolveIpcEndpoint("ces", {
1037
- workspaceDir,
1038
- platform: hostPlatform,
1039
- }).path;
1040
- }
1041
- const override = computeIpcSocketDirOverride(workspaceDir);
1042
- const socketDir = override ?? workspaceDir;
1043
- mkdirSync(socketDir, { recursive: true });
1044
- return join(socketDir, "ces.sock");
1050
+ return resolveIpcEndpoint("ces", {
1051
+ workspaceDir: resolveCesBootstrapSocketDir(resources),
1052
+ platform: hostPlatform,
1053
+ }).path;
1045
1054
  }
1046
1055
 
1047
1056
  async function isIpcEndpointReady(endpointPath: string): Promise<boolean> {
@@ -1101,13 +1110,15 @@ export async function startCes(
1101
1110
  const workspaceDir = resources
1102
1111
  ? join(resources.instanceDir, ".vellum", "workspace")
1103
1112
  : join(homedir(), ".vellum", "workspace");
1113
+ const bootstrapDir = resolveCesBootstrapSocketDir(resources);
1104
1114
  mkdirSync(securityDir, { recursive: true });
1115
+ mkdirSync(bootstrapDir, { recursive: true });
1105
1116
 
1106
1117
  const cesEnv: Record<string, string | undefined> = {
1107
1118
  ...process.env,
1108
- CES_LOCAL_SOCKET: socketPath,
1109
1119
  CREDENTIAL_SECURITY_DIR: securityDir,
1110
1120
  VELLUM_WORKSPACE_DIR: workspaceDir,
1121
+ CES_BOOTSTRAP_SOCKET_DIR: bootstrapDir,
1111
1122
  };
1112
1123
 
1113
1124
  let ces;
@@ -528,20 +528,8 @@ export async function injectCredentialsIntoAssistant(
528
528
  promises.push(inject("vellum:assistant_api_key", params.assistantApiKey));
529
529
  }
530
530
 
531
- promises.push(
532
- inject("vellum:platform_assistant_id", params.platformAssistantId),
533
- );
534
-
535
531
  promises.push(inject("vellum:platform_base_url", params.platformBaseUrl));
536
532
 
537
- promises.push(
538
- inject("vellum:platform_organization_id", params.organizationId),
539
- );
540
-
541
- if (params.userId) {
542
- promises.push(inject("vellum:platform_user_id", params.userId));
543
- }
544
-
545
533
  if (params.webhookSecret) {
546
534
  promises.push(inject("vellum:webhook_secret", params.webhookSecret));
547
535
  }
@@ -77,6 +77,7 @@ const PROVIDER_LABELS: Record<LlmProviderId, string> = {
77
77
  opencode: "OpenCode",
78
78
  baseten: "Baseten",
79
79
  poolside: "Poolside",
80
+ jev: "Jev",
80
81
  };
81
82
 
82
83
  // litellm and opencode are deliberately excluded: they have no fixed default
@@ -246,6 +247,9 @@ export function inferProviderFromModel(model: string): string | undefined {
246
247
  if (model.startsWith("poolside/")) {
247
248
  return "poolside";
248
249
  }
250
+ if (model === "jev-latest" || model.startsWith("jev-")) {
251
+ return "jev";
252
+ }
249
253
  if (model === "qwen/qwen3-8b") {
250
254
  // The only qwen/* ID the Vellum hosted provider carries; the rest are
251
255
  // OpenRouter's.
@@ -187,6 +187,7 @@ export const DOCKER_STATEFUL_SET_SPEC: DockerStatefulSetSpec = {
187
187
  { kind: "static", name: "VELLUM_BACKUP_DIR", value: "/workspace/.backups" },
188
188
  { kind: "static", name: "VELLUM_BACKUP_KEY_PATH", value: "/workspace/.backup.key" },
189
189
  { kind: "static", name: "CES_CREDENTIAL_URL", value: "http://localhost:8090" },
190
+ { kind: "static", name: "CES_BOOTSTRAP_SOCKET_DIR", value: "/run/ces-bootstrap" },
190
191
  { kind: "static", name: "GATEWAY_IPC_SOCKET_DIR", value: "/run/gateway-ipc" },
191
192
  { kind: "static", name: "ASSISTANT_IPC_SOCKET_DIR", value: "/run/assistant-ipc" },
192
193
  { kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Platform (cloud="vellum") half of the pre-teleport safety backup.
3
+ *
4
+ * A platform source is snapshotted through Django's user-facing backup
5
+ * endpoint, which cuts a PVC VolumeSnapshot via vembda. The POST returns as
6
+ * soon as the snapshot object exists; it becomes restorable asynchronously,
7
+ * so {@link createPlatformBackup} polls the listing until the new snapshot
8
+ * reports `ready_to_use` before returning. A snapshot that never becomes
9
+ * ready within the timeout is a backup failure, not a success.
10
+ *
11
+ * Local and docker sources are backed up host-side by `backup-ops` instead,
12
+ * so the restore point survives the source's retirement. Policy shared with
13
+ * the web teleport lives in `@vellumai/local-mode/teleport-backup-policy`.
14
+ */
15
+
16
+ import {
17
+ MANAGED_BACKUP_READY_POLL_INTERVAL_MS,
18
+ MANAGED_BACKUP_READY_TIMEOUT_MS,
19
+ managedBackupIsReady,
20
+ readyManagedBackupCreatedAts,
21
+ type ManagedBackupEntry,
22
+ } from "@vellumai/local-mode/teleport-backup-policy";
23
+
24
+ import type { AssistantEntry } from "./assistant-config.js";
25
+ import { loopbackSafeFetch } from "./loopback-fetch.js";
26
+ import { authHeaders, invalidateOrgIdCache } from "./platform-client.js";
27
+
28
+ type PlatformEntry = Pick<AssistantEntry, "runtimeUrl" | "assistantId">;
29
+
30
+ function platformBackupsUrl(entry: PlatformEntry): string {
31
+ return `${entry.runtimeUrl}/v1/assistants/${entry.assistantId}/backups/`;
32
+ }
33
+
34
+ async function errorSuffix(response: Response): Promise<string> {
35
+ const body = await response.text().catch(() => "");
36
+ return `(${response.status})${body ? `: ${body}` : ""}`;
37
+ }
38
+
39
+ /**
40
+ * Send a platform backup request with user-session auth, refreshing the
41
+ * cached organization id once on 401 (the same recovery the runtime identity
42
+ * probe applies).
43
+ */
44
+ async function platformBackupRequest(
45
+ entry: PlatformEntry,
46
+ token: string,
47
+ method: "GET" | "POST",
48
+ ): Promise<Response> {
49
+ const doRequest = async (): Promise<Response> =>
50
+ loopbackSafeFetch(platformBackupsUrl(entry), {
51
+ method,
52
+ headers: {
53
+ ...(await authHeaders(token, entry.runtimeUrl)),
54
+ Accept: "application/json",
55
+ },
56
+ });
57
+ let response = await doRequest();
58
+ if (response.status === 401) {
59
+ invalidateOrgIdCache(token, entry.runtimeUrl);
60
+ response = await doRequest();
61
+ }
62
+ return response;
63
+ }
64
+
65
+ async function fetchPlatformBackups(
66
+ entry: PlatformEntry,
67
+ token: string,
68
+ ): Promise<ManagedBackupEntry[]> {
69
+ const response = await platformBackupRequest(entry, token, "GET");
70
+ if (!response.ok) {
71
+ throw new Error(
72
+ `Platform backup list failed ${await errorSuffix(response)}`,
73
+ );
74
+ }
75
+ const body = (await response.json()) as { backups?: ManagedBackupEntry[] };
76
+ return body.backups ?? [];
77
+ }
78
+
79
+ /**
80
+ * `created_at` of every restorable PVC snapshot of a platform assistant:
81
+ * `GET /v1/assistants/<id>/backups/`.
82
+ */
83
+ export async function listPlatformBackups(
84
+ entry: PlatformEntry,
85
+ token: string,
86
+ ): Promise<Array<string | undefined>> {
87
+ return readyManagedBackupCreatedAts(await fetchPlatformBackups(entry, token));
88
+ }
89
+
90
+ export interface CreatePlatformBackupOptions {
91
+ timeoutMs?: number;
92
+ pollIntervalMs?: number;
93
+ }
94
+
95
+ /**
96
+ * Take a PVC snapshot of a platform assistant and wait until it is
97
+ * restorable: `POST /v1/assistants/<id>/backups/`, then re-list until the
98
+ * returned `snapshot_name` reports `ready_to_use`. Throws if the POST fails
99
+ * or the snapshot is not ready within `timeoutMs`.
100
+ */
101
+ export async function createPlatformBackup(
102
+ entry: PlatformEntry,
103
+ token: string,
104
+ options: CreatePlatformBackupOptions = {},
105
+ ): Promise<void> {
106
+ const timeoutMs = options.timeoutMs ?? MANAGED_BACKUP_READY_TIMEOUT_MS;
107
+ const pollIntervalMs =
108
+ options.pollIntervalMs ?? MANAGED_BACKUP_READY_POLL_INTERVAL_MS;
109
+
110
+ const response = await platformBackupRequest(entry, token, "POST");
111
+ if (!response.ok) {
112
+ throw new Error(
113
+ `Platform backup create failed ${await errorSuffix(response)}`,
114
+ );
115
+ }
116
+ const created = (await response.json().catch(() => null)) as {
117
+ snapshot_name?: string;
118
+ ready_to_use?: boolean;
119
+ } | null;
120
+ const snapshotName = created?.snapshot_name;
121
+ if (!snapshotName) {
122
+ throw new Error("Platform backup create returned no snapshot name");
123
+ }
124
+ if (created?.ready_to_use === true) {
125
+ return;
126
+ }
127
+
128
+ const deadline = Date.now() + timeoutMs;
129
+ while (Date.now() < deadline) {
130
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
131
+ const backups = await fetchPlatformBackups(entry, token);
132
+ if (managedBackupIsReady(backups, snapshotName)) {
133
+ return;
134
+ }
135
+ }
136
+ throw new Error(
137
+ `Platform backup ${snapshotName} was not ready after ${Math.round(timeoutMs / 1000)}s`,
138
+ );
139
+ }
@@ -34,6 +34,7 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
34
34
  opencode: "OPENCODE_API_KEY",
35
35
  baseten: "BASETEN_API_KEY",
36
36
  poolside: "POOLSIDE_API_KEY",
37
+ jev: "TYPESAFE_API_KEY",
37
38
  };
38
39
 
39
40
  /** Search-provider env var names. Mirrors `SEARCH_PROVIDER_CATALOG` BYOK entries. */