@vellumai/cli 0.5.12 → 0.5.14

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.
@@ -13,8 +13,6 @@ import {
13
13
  captureImageRefs,
14
14
  GATEWAY_INTERNAL_PORT,
15
15
  dockerResourceNames,
16
- migrateCesSecurityFiles,
17
- migrateGatewaySecurityFiles,
18
16
  startContainers,
19
17
  stopContainers,
20
18
  } from "../lib/docker";
@@ -417,12 +415,6 @@ async function upgradeDocker(
417
415
  }
418
416
  }
419
417
 
420
- console.log("🔄 Migrating security files to gateway volume...");
421
- await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
422
-
423
- console.log("🔄 Migrating credential files to CES security volume...");
424
- await migrateCesSecurityFiles(res, (msg) => console.log(msg));
425
-
426
418
  console.log("🚀 Starting upgraded containers...");
427
419
  await startContainers(
428
420
  {
@@ -522,9 +514,6 @@ async function upgradeDocker(
522
514
 
523
515
  await stopContainers(res);
524
516
 
525
- await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
526
- await migrateCesSecurityFiles(res, (msg) => console.log(msg));
527
-
528
517
  await startContainers(
529
518
  {
530
519
  signingKey,
@@ -729,9 +718,9 @@ async function upgradePlatform(
729
718
  process.exit(1);
730
719
  }
731
720
 
732
- const orgId = await fetchOrganizationId(token);
721
+ const orgId = await fetchOrganizationId(token, entry.runtimeUrl);
733
722
 
734
- const url = `${getPlatformUrl()}/v1/assistants/upgrade/`;
723
+ const url = `${entry.runtimeUrl || getPlatformUrl()}/v1/assistants/upgrade/`;
735
724
  const body: { assistant_id?: string; version?: string } = {
736
725
  assistant_id: entry.assistantId,
737
726
  };
@@ -1,10 +1,14 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
- import { resolveTargetAssistant } from "../lib/assistant-config.js";
4
+ import {
5
+ resolveTargetAssistant,
6
+ saveAssistantEntry,
7
+ } from "../lib/assistant-config.js";
5
8
  import { dockerResourceNames, wakeContainers } from "../lib/docker.js";
6
9
  import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
7
10
  import {
11
+ generateLocalSigningKey,
8
12
  isAssistantWatchModeAvailable,
9
13
  isGatewayWatchModeAvailable,
10
14
  startLocalDaemon,
@@ -106,8 +110,17 @@ export async function wake(): Promise<void> {
106
110
  }
107
111
  }
108
112
 
113
+ // Reuse the lockfile-persisted signing key so client actor tokens survive
114
+ // daemon/gateway restarts. Generate and persist a new one only on first wake.
115
+ let signingKey = resources.signingKey;
116
+ if (!signingKey) {
117
+ signingKey = generateLocalSigningKey();
118
+ entry.resources = { ...resources, signingKey };
119
+ saveAssistantEntry(entry);
120
+ }
121
+
109
122
  if (!daemonRunning) {
110
- await startLocalDaemon(watch, resources, { foreground });
123
+ await startLocalDaemon(watch, resources, { foreground, signingKey });
111
124
  }
112
125
 
113
126
  // Start gateway
@@ -127,13 +140,13 @@ export async function wake(): Promise<void> {
127
140
  `Gateway running (pid ${pid}) — restarting in watch mode...`,
128
141
  );
129
142
  await stopProcessByPidFile(gatewayPidFile, "gateway");
130
- await startGateway(watch, resources);
143
+ await startGateway(watch, resources, { signingKey });
131
144
  }
132
145
  } else {
133
146
  console.log(`Gateway already running (pid ${pid}).`);
134
147
  }
135
148
  } else {
136
- await startGateway(watch, resources);
149
+ await startGateway(watch, resources, { signingKey });
137
150
  }
138
151
  }
139
152
 
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import { rollback } from "./commands/rollback";
15
15
  import { setup } from "./commands/setup";
16
16
  import { sleep } from "./commands/sleep";
17
17
  import { ssh } from "./commands/ssh";
18
+ import { teleport } from "./commands/teleport";
18
19
  import { tunnel } from "./commands/tunnel";
19
20
  import { upgrade } from "./commands/upgrade";
20
21
  import { use } from "./commands/use";
@@ -44,6 +45,7 @@ const commands = {
44
45
  setup,
45
46
  sleep,
46
47
  ssh,
48
+ teleport,
47
49
  tunnel,
48
50
  upgrade,
49
51
  use,
@@ -76,6 +78,7 @@ function printHelp(): void {
76
78
  console.log(" setup Configure API keys interactively");
77
79
  console.log(" sleep Stop the assistant process");
78
80
  console.log(" ssh SSH into a remote assistant instance");
81
+ console.log(" teleport Transfer assistant data between environments");
79
82
  console.log(" tunnel Create a tunnel for a locally hosted assistant");
80
83
  console.log(" upgrade Upgrade an assistant to a newer version");
81
84
  console.log(" use Set the active assistant for commands");
@@ -16,6 +16,7 @@ import {
16
16
  DEFAULT_DAEMON_PORT,
17
17
  DEFAULT_GATEWAY_PORT,
18
18
  DEFAULT_QDRANT_PORT,
19
+ LOCKFILE_NAMES,
19
20
  } from "./constants.js";
20
21
  import { probePort } from "./port-probe.js";
21
22
 
@@ -42,6 +43,9 @@ export interface LocalInstanceResources {
42
43
  cesPort: number;
43
44
  /** Absolute path to the daemon PID file */
44
45
  pidFile: string;
46
+ /** Persisted HMAC signing key (hex). Survives daemon/gateway restarts so
47
+ * client actor tokens remain valid across `wake` cycles. */
48
+ signingKey?: string;
45
49
  [key: string]: unknown;
46
50
  }
47
51
 
@@ -76,8 +80,6 @@ export interface AssistantEntry {
76
80
  sshUser?: string;
77
81
  zone?: string;
78
82
  hatchedAt?: string;
79
- /** Name of the shared volume backing BASE_DATA_DIR for containerised instances. */
80
- volume?: string;
81
83
  /** Per-instance resource config. Present for local entries in multi-instance setups. */
82
84
  resources?: LocalInstanceResources;
83
85
  /** PID of the file watcher process for docker instances hatched with --watch. */
@@ -119,10 +121,7 @@ function getLockfileDir(): string {
119
121
 
120
122
  function readLockfile(): LockfileData {
121
123
  const base = getLockfileDir();
122
- const candidates = [
123
- join(base, ".vellum.lock.json"),
124
- join(base, ".vellum.lockfile.json"),
125
- ];
124
+ const candidates = LOCKFILE_NAMES.map((name) => join(base, name));
126
125
  for (const lockfilePath of candidates) {
127
126
  if (!existsSync(lockfilePath)) continue;
128
127
  try {
@@ -139,7 +138,7 @@ function readLockfile(): LockfileData {
139
138
  }
140
139
 
141
140
  function writeLockfile(data: LockfileData): void {
142
- const lockfilePath = join(getLockfileDir(), ".vellum.lock.json");
141
+ const lockfilePath = join(getLockfileDir(), LOCKFILE_NAMES[0]);
143
142
  const tmpPath = `${lockfilePath}.${randomBytes(4).toString("hex")}.tmp`;
144
143
  try {
145
144
  writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
@@ -18,6 +18,16 @@ export const DEFAULT_GATEWAY_PORT = 7830;
18
18
  export const DEFAULT_QDRANT_PORT = 6333;
19
19
  export const DEFAULT_CES_PORT = 8090;
20
20
 
21
+ /**
22
+ * Lockfile candidate filenames, checked in priority order.
23
+ * `.vellum.lock.json` is the current name; `.vellum.lockfile.json` is the
24
+ * legacy name kept for backwards compatibility with older installs.
25
+ */
26
+ export const LOCKFILE_NAMES = [
27
+ ".vellum.lock.json",
28
+ ".vellum.lockfile.json",
29
+ ] as const;
30
+
21
31
  /**
22
32
  * Environment variable names for provider API keys, keyed by provider ID.
23
33
  * Loaded from the shared registry at `meta/provider-env-vars.json` — the
@@ -33,6 +43,7 @@ export const VALID_REMOTE_HOSTS = [
33
43
  "aws",
34
44
  "docker",
35
45
  "custom",
46
+ "vellum",
36
47
  ] as const;
37
48
  export type RemoteHost = (typeof VALID_REMOTE_HOSTS)[number];
38
49
  export const VALID_SPECIES = ["openclaw", "vellum"] as const;
package/src/lib/docker.ts CHANGED
@@ -306,8 +306,6 @@ export function dockerResourceNames(instanceName: string) {
306
306
  assistantContainer: `${instanceName}-assistant`,
307
307
  cesContainer: `${instanceName}-credential-executor`,
308
308
  cesSecurityVolume: `${instanceName}-ces-sec`,
309
- /** @deprecated Legacy — no longer created for new instances. Retained for migration of existing instances. */
310
- dataVolume: `${instanceName}-data`,
311
309
  gatewayContainer: `${instanceName}-gateway`,
312
310
  gatewaySecurityVolume: `${instanceName}-gateway-sec`,
313
311
  network: `${instanceName}-net`,
@@ -363,7 +361,6 @@ export async function retireDocker(name: string): Promise<void> {
363
361
  // network may not exist
364
362
  }
365
363
  for (const vol of [
366
- res.dataVolume,
367
364
  res.socketVolume,
368
365
  res.workspaceVolume,
369
366
  res.cesSecurityVolume,
@@ -519,6 +516,8 @@ export function serviceDockerRunArgs(opts: {
519
516
  "-v",
520
517
  `${res.socketVolume}:/run/ces-bootstrap`,
521
518
  "-e",
519
+ "IS_CONTAINERIZED=false",
520
+ "-e",
522
521
  `VELLUM_ASSISTANT_NAME=${instanceName}`,
523
522
  "-e",
524
523
  "VELLUM_CLOUD=docker",
@@ -629,148 +628,6 @@ export function serviceDockerRunArgs(opts: {
629
628
  };
630
629
  }
631
630
 
632
- /**
633
- * Check whether a Docker volume exists.
634
- * Returns true if the volume exists, false otherwise.
635
- */
636
- async function dockerVolumeExists(volumeName: string): Promise<boolean> {
637
- try {
638
- await execOutput("docker", ["volume", "inspect", volumeName]);
639
- return true;
640
- } catch {
641
- return false;
642
- }
643
- }
644
-
645
- /**
646
- * Migrate trust.json and actor-token-signing-key from the data volume
647
- * (old location: /data/.vellum/protected/) to the gateway security volume
648
- * (new location: /gateway-security/).
649
- *
650
- * Uses a temporary busybox container that mounts both volumes. The migration
651
- * is idempotent: it only copies a file when the source exists on the data
652
- * volume and the destination does not yet exist on the gateway security volume.
653
- *
654
- * Skips migration entirely if the data volume does not exist (new instances
655
- * no longer create one).
656
- */
657
- export async function migrateGatewaySecurityFiles(
658
- res: ReturnType<typeof dockerResourceNames>,
659
- log: (msg: string) => void,
660
- ): Promise<void> {
661
- // New instances don't have a data volume — nothing to migrate.
662
- if (!(await dockerVolumeExists(res.dataVolume))) {
663
- log(" No data volume found — skipping gateway security migration.");
664
- return;
665
- }
666
-
667
- const migrationContainer = `${res.gatewayContainer}-migration`;
668
- const filesToMigrate = ["trust.json", "actor-token-signing-key"];
669
-
670
- // Remove any leftover migration container from a previous interrupted run.
671
- try {
672
- await exec("docker", ["rm", "-f", migrationContainer]);
673
- } catch {
674
- // container may not exist
675
- }
676
-
677
- for (const fileName of filesToMigrate) {
678
- const src = `/data/.vellum/protected/${fileName}`;
679
- const dst = `/gateway-security/${fileName}`;
680
-
681
- try {
682
- // Run a busybox container that checks source exists and destination
683
- // does not, then copies. The shell exits 0 whether or not a copy
684
- // happens, so the migration is always safe to re-run.
685
- await exec("docker", [
686
- "run",
687
- "--rm",
688
- "--name",
689
- migrationContainer,
690
- "-v",
691
- `${res.dataVolume}:/data:ro`,
692
- "-v",
693
- `${res.gatewaySecurityVolume}:/gateway-security`,
694
- "busybox",
695
- "sh",
696
- "-c",
697
- `if [ -f "${src}" ] && [ ! -f "${dst}" ]; then cp "${src}" "${dst}" && echo "migrated"; else echo "skipped"; fi`,
698
- ]);
699
- log(` ${fileName}: checked`);
700
- } catch (err) {
701
- // Non-fatal — log and continue. The gateway will create fresh files
702
- // if they don't exist.
703
- const message = err instanceof Error ? err.message : String(err);
704
- log(` ${fileName}: migration failed (${message}), continuing...`);
705
- }
706
- }
707
- }
708
-
709
- /**
710
- * Migrate keys.enc and store.key from the data volume
711
- * (old location: /data/.vellum/protected/) to the CES security volume
712
- * (new location: /ces-security/).
713
- *
714
- * Uses a temporary busybox container that mounts both volumes. The migration
715
- * is idempotent: it only copies a file when the source exists on the data
716
- * volume and the destination does not yet exist on the CES security volume.
717
- * Migrated files are chowned to 1001:1001 (the CES service user).
718
- *
719
- * Skips migration entirely if the data volume does not exist (new instances
720
- * no longer create one).
721
- */
722
- export async function migrateCesSecurityFiles(
723
- res: ReturnType<typeof dockerResourceNames>,
724
- log: (msg: string) => void,
725
- ): Promise<void> {
726
- // New instances don't have a data volume — nothing to migrate.
727
- if (!(await dockerVolumeExists(res.dataVolume))) {
728
- log(" No data volume found — skipping CES security migration.");
729
- return;
730
- }
731
-
732
- const migrationContainer = `${res.cesContainer}-migration`;
733
- const filesToMigrate = ["keys.enc", "store.key"];
734
-
735
- // Remove any leftover migration container from a previous interrupted run.
736
- try {
737
- await exec("docker", ["rm", "-f", migrationContainer]);
738
- } catch {
739
- // container may not exist
740
- }
741
-
742
- for (const fileName of filesToMigrate) {
743
- const src = `/data/.vellum/protected/${fileName}`;
744
- const dst = `/ces-security/${fileName}`;
745
-
746
- try {
747
- // Run a busybox container that checks source exists and destination
748
- // does not, then copies and sets ownership. The shell exits 0 whether
749
- // or not a copy happens, so the migration is always safe to re-run.
750
- await exec("docker", [
751
- "run",
752
- "--rm",
753
- "--name",
754
- migrationContainer,
755
- "-v",
756
- `${res.dataVolume}:/data:ro`,
757
- "-v",
758
- `${res.cesSecurityVolume}:/ces-security`,
759
- "busybox",
760
- "sh",
761
- "-c",
762
- `if [ -f "${src}" ] && [ ! -f "${dst}" ]; then cp "${src}" "${dst}" && chown 1001:1001 "${dst}" && echo "migrated"; else echo "skipped"; fi`,
763
- ]);
764
- log(` ${fileName}: checked`);
765
- } catch (err) {
766
- // Non-fatal — log and continue. The CES will start without
767
- // credentials if they don't exist.
768
- const message = err instanceof Error ? err.message : String(err);
769
- log(` ${fileName}: migration failed (${message}), continuing...`);
770
- }
771
- }
772
- }
773
-
774
631
  /** The order in which services must be started. */
775
632
  export const SERVICE_START_ORDER: ServiceName[] = [
776
633
  "assistant",
@@ -1202,7 +1059,6 @@ export async function hatchDocker(
1202
1059
  cloud: "docker",
1203
1060
  species,
1204
1061
  hatchedAt: new Date().toISOString(),
1205
- volume: res.dataVolume,
1206
1062
  serviceGroupVersion: cliPkg.version ? `v${cliPkg.version}` : undefined,
1207
1063
  containerInfo: {
1208
1064
  assistantImage: imageTags.assistant,
@@ -28,7 +28,7 @@ export async function checkManagedHealth(
28
28
  let orgId: string;
29
29
  try {
30
30
  const { fetchOrganizationId } = await import("./platform-client.js");
31
- orgId = await fetchOrganizationId(token);
31
+ orgId = await fetchOrganizationId(token, runtimeUrl);
32
32
  } catch (err) {
33
33
  return {
34
34
  status: "error (auth)",
package/src/lib/local.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync, execSync, spawn } from "child_process";
2
+ import { randomBytes } from "crypto";
2
3
  import {
3
4
  existsSync,
4
5
  mkdirSync,
@@ -192,9 +193,24 @@ function resolveDaemonMainPath(assistantIndex: string): string {
192
193
  return join(dirname(assistantIndex), "daemon", "main.ts");
193
194
  }
194
195
 
196
+ /**
197
+ * Generate a fresh signing key for a local hatch session.
198
+ *
199
+ * Both the daemon and gateway must use the same HMAC signing key so JWT
200
+ * tokens minted by one can be verified by the other. The CLI generates
201
+ * an ephemeral key each time and passes it as `ACTOR_TOKEN_SIGNING_KEY`
202
+ * to both processes — the daemon and gateway each persist it on their
203
+ * own terms (the `.vellum/` directory layout is their concern, not the
204
+ * CLI's).
205
+ */
206
+ export function generateLocalSigningKey(): string {
207
+ return randomBytes(32).toString("hex");
208
+ }
209
+
195
210
  type DaemonStartOptions = {
196
211
  foreground?: boolean;
197
212
  defaultWorkspaceConfigPath?: string;
213
+ signingKey?: string;
198
214
  };
199
215
 
200
216
  async function startDaemonFromSource(
@@ -267,6 +283,9 @@ async function startDaemonFromSource(
267
283
  RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
268
284
  VELLUM_CLOUD: "local",
269
285
  VELLUM_DEV: "1",
286
+ ...(options?.signingKey
287
+ ? { ACTOR_TOKEN_SIGNING_KEY: options.signingKey }
288
+ : {}),
270
289
  };
271
290
  if (resources) {
272
291
  env.BASE_DATA_DIR = resources.instanceDir;
@@ -385,6 +404,9 @@ async function startDaemonWatchFromSource(
385
404
  ...process.env,
386
405
  RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
387
406
  VELLUM_DEV: "1",
407
+ ...(options?.signingKey
408
+ ? { ACTOR_TOKEN_SIGNING_KEY: options.signingKey }
409
+ : {}),
388
410
  };
389
411
  if (resources) {
390
412
  env.BASE_DATA_DIR = resources.instanceDir;
@@ -970,6 +992,10 @@ export async function startLocalDaemon(
970
992
  delete daemonEnv.QDRANT_URL;
971
993
  }
972
994
 
995
+ if (options?.signingKey) {
996
+ daemonEnv.ACTOR_TOKEN_SIGNING_KEY = options.signingKey;
997
+ }
998
+
973
999
  // Write a sentinel PID file before spawning so concurrent hatch() calls
974
1000
  // see the file and fall through to the isDaemonResponsive() port check
975
1001
  // instead of racing to spawn a duplicate daemon.
@@ -1081,6 +1107,7 @@ export async function startLocalDaemon(
1081
1107
  export async function startGateway(
1082
1108
  watch: boolean = false,
1083
1109
  resources?: LocalInstanceResources,
1110
+ options?: { signingKey?: string },
1084
1111
  ): Promise<string> {
1085
1112
  const effectiveGatewayPort = resources?.gatewayPort ?? GATEWAY_PORT;
1086
1113
 
@@ -1117,9 +1144,12 @@ export async function startGateway(
1117
1144
  ...(process.env as Record<string, string>),
1118
1145
  RUNTIME_HTTP_PORT: String(effectiveDaemonPort),
1119
1146
  GATEWAY_PORT: String(effectiveGatewayPort),
1147
+ ...(options?.signingKey
1148
+ ? { ACTOR_TOKEN_SIGNING_KEY: options.signingKey }
1149
+ : {}),
1120
1150
  ...(watch ? { VELLUM_DEV: "1" } : {}),
1121
- // Set BASE_DATA_DIR so the gateway loads the correct signing key and
1122
- // credentials for this instance (mirrors the daemon env setup).
1151
+ // Set BASE_DATA_DIR so the gateway loads the correct credentials and
1152
+ // workspace config for this instance (mirrors the daemon env setup).
1123
1153
  ...(resources ? { BASE_DATA_DIR: resources.instanceDir } : {}),
1124
1154
  };
1125
1155
  // The gateway reads the ingress URL from the workspace config file via
@@ -65,6 +65,67 @@ export function clearPlatformToken(): void {
65
65
  }
66
66
  }
67
67
 
68
+ const VAK_PREFIX = "vak_";
69
+
70
+ /**
71
+ * Returns the appropriate auth header for the given platform token.
72
+ *
73
+ * - `vak_`-prefixed tokens are long-lived platform API keys and use
74
+ * `Authorization: Bearer`.
75
+ * - All other tokens are allauth session tokens and use `X-Session-Token`.
76
+ */
77
+ export function authHeaders(token: string): Record<string, string> {
78
+ if (token.startsWith(VAK_PREFIX)) {
79
+ return { Authorization: `Bearer ${token}` };
80
+ }
81
+ return { "X-Session-Token": token };
82
+ }
83
+
84
+ export interface HatchedAssistant {
85
+ id: string;
86
+ name: string;
87
+ status: string;
88
+ }
89
+
90
+ export async function hatchAssistant(token: string): Promise<HatchedAssistant> {
91
+ const url = `${getPlatformUrl()}/v1/assistants/hatch/`;
92
+
93
+ const response = await fetch(url, {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ ...authHeaders(token),
98
+ },
99
+ body: JSON.stringify({}),
100
+ });
101
+
102
+ if (response.ok) {
103
+ return (await response.json()) as HatchedAssistant;
104
+ }
105
+
106
+ if (response.status === 401 || response.status === 403) {
107
+ const detail = (await response.json().catch(() => ({}))) as {
108
+ detail?: string;
109
+ };
110
+ throw new Error(
111
+ detail.detail ??
112
+ "Invalid or expired token. Run `vellum login` to re-authenticate.",
113
+ );
114
+ }
115
+
116
+ if (response.status === 402) {
117
+ throw new Error("Insufficient balance to hatch a new assistant.");
118
+ }
119
+
120
+ const errorBody = (await response.json().catch(() => ({}))) as {
121
+ detail?: string;
122
+ };
123
+ throw new Error(
124
+ errorBody.detail ??
125
+ `Platform API error: ${response.status} ${response.statusText}`,
126
+ );
127
+ }
128
+
68
129
  export interface PlatformUser {
69
130
  id: string;
70
131
  email: string;
@@ -75,16 +136,19 @@ interface OrganizationListResponse {
75
136
  results: { id: string; name: string }[];
76
137
  }
77
138
 
78
- export async function fetchOrganizationId(token: string): Promise<string> {
79
- const platformUrl = getPlatformUrl();
80
- const url = `${platformUrl}/v1/organizations/`;
139
+ export async function fetchOrganizationId(
140
+ token: string,
141
+ platformUrl?: string,
142
+ ): Promise<string> {
143
+ const resolvedUrl = platformUrl || getPlatformUrl();
144
+ const url = `${resolvedUrl}/v1/organizations/`;
81
145
  const response = await fetch(url, {
82
146
  headers: { "X-Session-Token": token },
83
147
  });
84
148
 
85
149
  if (!response.ok) {
86
150
  throw new Error(
87
- `Failed to fetch organizations from ${platformUrl} (${response.status}). Try logging in again.`,
151
+ `Failed to fetch organizations from ${resolvedUrl} (${response.status}). Try logging in again.`,
88
152
  );
89
153
  }
90
154
 
@@ -107,8 +171,12 @@ interface AllauthSessionResponse {
107
171
  };
108
172
  }
109
173
 
110
- export async function fetchCurrentUser(token: string): Promise<PlatformUser> {
111
- const url = `${getPlatformUrl()}/_allauth/app/v1/auth/session`;
174
+ export async function fetchCurrentUser(
175
+ token: string,
176
+ platformUrl?: string,
177
+ ): Promise<PlatformUser> {
178
+ const resolvedUrl = platformUrl || getPlatformUrl();
179
+ const url = `${resolvedUrl}/_allauth/app/v1/auth/session`;
112
180
  const response = await fetch(url, {
113
181
  headers: { "X-Session-Token": token },
114
182
  });
@@ -138,9 +206,10 @@ export async function rollbackPlatformAssistant(
138
206
  token: string,
139
207
  orgId: string,
140
208
  version?: string,
209
+ platformUrl?: string,
141
210
  ): Promise<{ detail: string; version: string | null }> {
142
- const platformUrl = getPlatformUrl();
143
- const response = await fetch(`${platformUrl}/v1/assistants/rollback/`, {
211
+ const resolvedUrl = platformUrl || getPlatformUrl();
212
+ const response = await fetch(`${resolvedUrl}/v1/assistants/rollback/`, {
144
213
  method: "POST",
145
214
  headers: {
146
215
  "Content-Type": "application/json",
@@ -182,9 +251,10 @@ export async function platformInitiateExport(
182
251
  token: string,
183
252
  orgId: string,
184
253
  description?: string,
254
+ platformUrl?: string,
185
255
  ): Promise<{ jobId: string; status: string }> {
186
- const platformUrl = getPlatformUrl();
187
- const response = await fetch(`${platformUrl}/v1/migrations/export/`, {
256
+ const resolvedUrl = platformUrl || getPlatformUrl();
257
+ const response = await fetch(`${resolvedUrl}/v1/migrations/export/`, {
188
258
  method: "POST",
189
259
  headers: {
190
260
  "Content-Type": "application/json",
@@ -215,10 +285,11 @@ export async function platformPollExportStatus(
215
285
  jobId: string,
216
286
  token: string,
217
287
  orgId: string,
288
+ platformUrl?: string,
218
289
  ): Promise<{ status: string; downloadUrl?: string; error?: string }> {
219
- const platformUrl = getPlatformUrl();
290
+ const resolvedUrl = platformUrl || getPlatformUrl();
220
291
  const response = await fetch(
221
- `${platformUrl}/v1/migrations/export/${jobId}/status/`,
292
+ `${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
222
293
  {
223
294
  headers: {
224
295
  "X-Session-Token": token,
@@ -269,10 +340,11 @@ export async function platformImportPreflight(
269
340
  bundleData: Uint8Array<ArrayBuffer>,
270
341
  token: string,
271
342
  orgId: string,
343
+ platformUrl?: string,
272
344
  ): Promise<{ statusCode: number; body: Record<string, unknown> }> {
273
- const platformUrl = getPlatformUrl();
345
+ const resolvedUrl = platformUrl || getPlatformUrl();
274
346
  const response = await fetch(
275
- `${platformUrl}/v1/migrations/import-preflight/`,
347
+ `${resolvedUrl}/v1/migrations/import-preflight/`,
276
348
  {
277
349
  method: "POST",
278
350
  headers: {
@@ -296,9 +368,10 @@ export async function platformImportBundle(
296
368
  bundleData: Uint8Array<ArrayBuffer>,
297
369
  token: string,
298
370
  orgId: string,
371
+ platformUrl?: string,
299
372
  ): Promise<{ statusCode: number; body: Record<string, unknown> }> {
300
- const platformUrl = getPlatformUrl();
301
- const response = await fetch(`${platformUrl}/v1/migrations/import/`, {
373
+ const resolvedUrl = platformUrl || getPlatformUrl();
374
+ const response = await fetch(`${resolvedUrl}/v1/migrations/import/`, {
302
375
  method: "POST",
303
376
  headers: {
304
377
  "Content-Type": "application/octet-stream",
@@ -9,8 +9,6 @@ import {
9
9
  DOCKER_READY_TIMEOUT_MS,
10
10
  dockerResourceNames,
11
11
  GATEWAY_INTERNAL_PORT,
12
- migrateCesSecurityFiles,
13
- migrateGatewaySecurityFiles,
14
12
  startContainers,
15
13
  stopContainers,
16
14
  } from "./docker.js";
@@ -573,12 +571,6 @@ export async function performDockerRollback(
573
571
  await stopContainers(res);
574
572
  console.log("✅ Containers stopped\n");
575
573
 
576
- console.log("🔄 Migrating security files to gateway volume...");
577
- await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
578
-
579
- console.log("🔄 Migrating credential files to CES security volume...");
580
- await migrateCesSecurityFiles(res, (msg) => console.log(msg));
581
-
582
574
  console.log("🚀 Starting containers with target version...");
583
575
  await startContainers(
584
576
  {
@@ -700,9 +692,6 @@ export async function performDockerRollback(
700
692
 
701
693
  await stopContainers(res);
702
694
 
703
- await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
704
- await migrateCesSecurityFiles(res, (msg) => console.log(msg));
705
-
706
695
  await startContainers(
707
696
  {
708
697
  signingKey,