@vellumai/cli 0.10.2-dev.202606251104.36cd100 → 0.10.2-dev.202606251257.2eba8a4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.2-dev.202606251104.36cd100",
3
+ "version": "0.10.2-dev.202606251257.2eba8a4",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -111,6 +111,7 @@ describe("getBuilderManagedEnvKeys", () => {
111
111
  describe("buildServiceRunArgs extra env routing", () => {
112
112
  const opts: BuildServiceRunArgsOpts = {
113
113
  gatewayPort: 18080,
114
+ assistantPort: 18081,
114
115
  imageTags: {
115
116
  assistant: "assistant:test",
116
117
  gateway: "gateway:test",
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  captureImageRefs,
10
10
  GATEWAY_INTERNAL_PORT,
11
+ ASSISTANT_INTERNAL_PORT,
11
12
  dockerResourceNames,
12
13
  startContainers,
13
14
  stopContainers,
@@ -324,6 +325,9 @@ export async function rollback(): Promise<void> {
324
325
  // use default
325
326
  }
326
327
 
328
+ // Recover the assistant host port from the entry, fall back to default.
329
+ const assistantPort = entry.containerInfo?.assistantPort ?? ASSISTANT_INTERNAL_PORT;
330
+
327
331
  // Notify connected clients that a rollback is about to begin (best-effort)
328
332
  console.log("📢 Notifying connected clients...");
329
333
  await broadcastUpgradeEvent(
@@ -373,6 +377,7 @@ export async function rollback(): Promise<void> {
373
377
  extraAssistantEnv,
374
378
  extraGatewayEnv,
375
379
  gatewayPort,
380
+ assistantPort,
376
381
  imageTags: previousImageRefs,
377
382
  instanceName,
378
383
  res,
@@ -399,6 +404,7 @@ export async function rollback(): Promise<void> {
399
404
  gatewayDigest: newDigests?.gateway,
400
405
  cesDigest: newDigests?.["credential-executor"],
401
406
  networkName: res.network,
407
+ assistantPort,
402
408
  },
403
409
  previousContainerInfo: entry.containerInfo,
404
410
  // Clear the backup path — it belonged to the upgrade we just rolled back
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  captureImageRefs,
16
16
  GATEWAY_INTERNAL_PORT,
17
+ ASSISTANT_INTERNAL_PORT,
17
18
  dockerResourceNames,
18
19
  startContainers,
19
20
  stopContainers,
@@ -432,6 +433,9 @@ async function upgradeDocker(
432
433
  // use default
433
434
  }
434
435
 
436
+ // Recover the assistant host port from the entry, fall back to default.
437
+ const assistantPort = entry.containerInfo?.assistantPort ?? ASSISTANT_INTERNAL_PORT;
438
+
435
439
  // Create pre-upgrade backup (best-effort, daemon must be running)
436
440
  await broadcastUpgradeEvent(
437
441
  entry.runtimeUrl,
@@ -483,6 +487,7 @@ async function upgradeDocker(
483
487
  extraAssistantEnv,
484
488
  extraGatewayEnv,
485
489
  gatewayPort,
490
+ assistantPort,
486
491
  imageTags,
487
492
  instanceName,
488
493
  res,
@@ -506,6 +511,7 @@ async function upgradeDocker(
506
511
  gatewayDigest: newDigests?.gateway,
507
512
  cesDigest: newDigests?.["credential-executor"],
508
513
  networkName: res.network,
514
+ assistantPort,
509
515
  },
510
516
  previousContainerInfo: entry.containerInfo,
511
517
  previousDbMigrationVersion: preMigrationState.dbVersion,
@@ -589,6 +595,7 @@ async function upgradeDocker(
589
595
  extraAssistantEnv,
590
596
  extraGatewayEnv,
591
597
  gatewayPort,
598
+ assistantPort,
592
599
  imageTags: previousImageRefs,
593
600
  instanceName,
594
601
  res,
@@ -654,6 +661,7 @@ async function upgradeDocker(
654
661
  rollbackDigests?.["credential-executor"] ??
655
662
  previousImageRefs["credential-executor"],
656
663
  networkName: res.network,
664
+ assistantPort,
657
665
  },
658
666
  previousContainerInfo: undefined,
659
667
  previousDbMigrationVersion: undefined,
@@ -27,6 +27,7 @@ function buildAssistantArgs(
27
27
  const res = dockerResourceNames(instanceName);
28
28
  const builders = buildServiceRunArgs({
29
29
  gatewayPort: 7830,
30
+ assistantPort: 7821,
30
31
  imageTags,
31
32
  instanceName,
32
33
  res,
@@ -41,6 +42,7 @@ function buildGatewayArgs(
41
42
  const res = dockerResourceNames(instanceName);
42
43
  const builders = buildServiceRunArgs({
43
44
  gatewayPort: 7830,
45
+ assistantPort: 7821,
44
46
  imageTags,
45
47
  instanceName,
46
48
  res,
@@ -84,13 +86,15 @@ describe("buildServiceRunArgs — assistant", () => {
84
86
  });
85
87
 
86
88
  test("publishes the assistant HTTP port on all host interfaces so sibling bot containers can reach the daemon via host.docker.internal on both Docker Desktop and Linux", () => {
87
- const args = buildAssistantArgs();
89
+ const args = buildAssistantArgs({ assistantPort: 18000 });
88
90
  // The port mapping is expressed as two adjacent args: "-p" then the spec.
89
91
  // Bound to all interfaces (no `127.0.0.1:` prefix) because on vanilla
90
92
  // Linux Docker, host.docker.internal:host-gateway resolves to the Docker
91
93
  // bridge gateway IP — packets arrive at the bridge interface, not
92
94
  // loopback, so a 127.0.0.1 DNAT rule would not match.
93
- const portSpec = `${ASSISTANT_INTERNAL_PORT}:${ASSISTANT_INTERNAL_PORT}`;
95
+ // The host-side port is dynamically allocated (not fixed at 7821) so
96
+ // concurrent instances on the same host don't collide.
97
+ const portSpec = `18000:${ASSISTANT_INTERNAL_PORT}`;
94
98
  const portIndex = args.indexOf(portSpec);
95
99
  expect(portIndex).toBeGreaterThan(0);
96
100
  expect(args[portIndex - 1]).toBe("-p");
@@ -74,6 +74,14 @@ export interface ContainerInfo {
74
74
  cesDigest?: string;
75
75
  /** Docker network name for the service group */
76
76
  networkName?: string;
77
+ /**
78
+ * Host-side port the assistant HTTP API is published on. Dynamically
79
+ * allocated at hatch time so concurrent instances don't collide on the
80
+ * default (7821). Stored so rollback/upgrade can rebind the same port
81
+ * instead of re-allocating (which could grab a different port if another
82
+ * process took it in the interim).
83
+ */
84
+ assistantPort?: number;
77
85
  }
78
86
 
79
87
  /**
package/src/lib/docker.ts CHANGED
@@ -23,7 +23,7 @@ import { buildHatchConfigValues, writeInitialConfig } from "./config-utils";
23
23
  import { buildServiceRunArgs } from "./statefulset.js";
24
24
  import type { Species } from "./constants";
25
25
  import { getOrCreateHostDeviceId } from "./device-id.js";
26
- import { getDefaultPorts } from "./environments/paths.js";
26
+ import { ASSISTANT_INTERNAL_PORT, getDefaultPorts } from "./environments/paths.js";
27
27
  import { getCurrentEnvironment } from "./environments/resolve.js";
28
28
  import { leaseGuardianToken } from "./guardian-token";
29
29
  import { logHatchNextSteps } from "./hatch-next-steps.js";
@@ -711,6 +711,7 @@ export async function startContainers(
711
711
  extraAssistantEnv?: Record<string, string>;
712
712
  extraGatewayEnv?: Record<string, string>;
713
713
  gatewayPort: number;
714
+ assistantPort: number;
714
715
  imageTags: Record<ServiceName, string>;
715
716
  instanceName: string;
716
717
  res: ReturnType<typeof dockerResourceNames>;
@@ -934,6 +935,7 @@ function startFileWatcher(opts: {
934
935
  extraAssistantEnv?: Record<string, string>;
935
936
  extraGatewayEnv?: Record<string, string>;
936
937
  gatewayPort: number;
938
+ assistantPort: number;
937
939
  imageTags: Record<ServiceName, string>;
938
940
  instanceName: string;
939
941
  repoRoot: string;
@@ -941,7 +943,7 @@ function startFileWatcher(opts: {
941
943
  netnsContainer?: string;
942
944
  assistantCaCertPath?: string;
943
945
  }): () => void {
944
- const { gatewayPort, imageTags, instanceName, repoRoot, res } = opts;
946
+ const { gatewayPort, assistantPort, imageTags, instanceName, repoRoot, res } = opts;
945
947
 
946
948
  const { dirs: watchDirs, files: watchFiles } = collectWatchTargets(repoRoot);
947
949
 
@@ -957,6 +959,7 @@ function startFileWatcher(opts: {
957
959
  extraAssistantEnv: opts.extraAssistantEnv,
958
960
  extraGatewayEnv: opts.extraGatewayEnv,
959
961
  gatewayPort,
962
+ assistantPort,
960
963
  imageTags,
961
964
  instanceName,
962
965
  res,
@@ -1136,6 +1139,30 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1136
1139
  }
1137
1140
  }
1138
1141
 
1142
+ // Allocate the assistant HTTP API host port. Same dynamic-allocation
1143
+ // strategy as the gateway port: the env-default (production 7821 /
1144
+ // non-prod overrides) is the *preferred* starting point, and we walk
1145
+ // upward until we find a free port. Without this, two concurrent
1146
+ // `vellum hatch --remote docker` on the same host collide on a fixed
1147
+ // 7821 bind ("port is already allocated"). Unused when netnsContainer
1148
+ // is set — no host ports are published in that mode.
1149
+ let assistantPort: number;
1150
+ if (params.netnsContainer) {
1151
+ assistantPort = ASSISTANT_INTERNAL_PORT;
1152
+ } else {
1153
+ const preferredAssistantPort = getDefaultPorts(
1154
+ getCurrentEnvironment(),
1155
+ ).daemon;
1156
+ assistantPort = await findOpenPort(preferredAssistantPort, {
1157
+ exclude: [gatewayPort],
1158
+ });
1159
+ if (assistantPort !== preferredAssistantPort) {
1160
+ log(
1161
+ `Preferred assistant port ${preferredAssistantPort} is in use; allocated ${assistantPort} for this instance.`,
1162
+ );
1163
+ }
1164
+ }
1165
+
1139
1166
  const imageTags: Record<ServiceName, string> = {
1140
1167
  assistant: "",
1141
1168
  "credential-executor": "",
@@ -1404,6 +1431,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1404
1431
  extraAssistantEnv,
1405
1432
  extraGatewayEnv,
1406
1433
  gatewayPort,
1434
+ assistantPort,
1407
1435
  imageTags,
1408
1436
  instanceName,
1409
1437
  res,
@@ -1432,6 +1460,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1432
1460
  gatewayDigest: imageDigests?.gateway,
1433
1461
  cesDigest: imageDigests?.["credential-executor"],
1434
1462
  networkName: res.network,
1463
+ assistantPort,
1435
1464
  },
1436
1465
  };
1437
1466
  emitProgress(5, 6, "Saving configuration...");
@@ -1502,6 +1531,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1502
1531
  extraAssistantEnv,
1503
1532
  extraGatewayEnv,
1504
1533
  gatewayPort,
1534
+ assistantPort,
1505
1535
  imageTags,
1506
1536
  instanceName,
1507
1537
  repoRoot,
@@ -21,10 +21,11 @@ import { createServer } from "net";
21
21
  */
22
22
  export async function findOpenPort(
23
23
  preferred: number,
24
- options: { maxAttempts?: number; host?: string } = {},
24
+ options: { maxAttempts?: number; host?: string; exclude?: number[] } = {},
25
25
  ): Promise<number> {
26
26
  const maxAttempts = options.maxAttempts ?? 50;
27
27
  const host = options.host ?? "0.0.0.0";
28
+ const exclude = new Set(options.exclude ?? []);
28
29
 
29
30
  if (!Number.isInteger(preferred) || preferred < 1 || preferred > 65535) {
30
31
  throw new Error(
@@ -41,6 +42,7 @@ export async function findOpenPort(
41
42
  for (let offset = 0; offset < maxAttempts; offset++) {
42
43
  const port = preferred + offset;
43
44
  if (port > 65535) break;
45
+ if (exclude.has(port)) continue;
44
46
  try {
45
47
  await probePort(port, host);
46
48
  return port;
@@ -85,8 +85,9 @@ interface VolumeMount {
85
85
  interface PortSpec {
86
86
  containerPort: number;
87
87
  /**
88
- * Host-side port. Literal string = use as-is. `"{{ gatewayPort }}"` is
89
- * a sentinel replaced with the instance-specific gateway port at build time.
88
+ * Host-side port. Literal string = use as-is. `"{{ gatewayPort }}"` and
89
+ * `"{{ assistantPort }}"` are sentinels replaced with the instance-specific
90
+ * gateway / assistant host ports at build time.
90
91
  */
91
92
  hostPort?: string;
92
93
  description?: string;
@@ -172,7 +173,7 @@ export const DOCKER_STATEFUL_SET_SPEC: DockerStatefulSetSpec = {
172
173
  },
173
174
  {
174
175
  containerPort: ASSISTANT_INTERNAL_PORT,
175
- hostPort: `${ASSISTANT_INTERNAL_PORT}`,
176
+ hostPort: "{{ assistantPort }}",
176
177
  description: "Assistant HTTP API",
177
178
  },
178
179
  ],
@@ -261,6 +262,13 @@ export const DOCKER_STATEFUL_SET_SPEC: DockerStatefulSetSpec = {
261
262
 
262
263
  export interface BuildServiceRunArgsOpts extends DockerRunSecrets {
263
264
  gatewayPort: number;
265
+ /**
266
+ * Host-side port for the assistant HTTP API. Allocated dynamically by
267
+ * `hatchDocker` (mirroring `gatewayPort`) so concurrent instances on the
268
+ * same host don't collide on a fixed port bind. Unused when
269
+ * `netnsContainer` is set (no host ports are published in that mode).
270
+ */
271
+ assistantPort: number;
264
272
  imageTags: Record<ServiceName, string>;
265
273
  instanceName: string;
266
274
  res: DockerResourceNames;
@@ -353,6 +361,7 @@ export function buildServiceRunArgs(
353
361
  ): Record<ServiceName, () => string[]> {
354
362
  const {
355
363
  gatewayPort,
364
+ assistantPort,
356
365
  imageTags,
357
366
  instanceName,
358
367
  res,
@@ -396,7 +405,9 @@ export function buildServiceRunArgs(
396
405
  const hostSide =
397
406
  port.hostPort === "{{ gatewayPort }}"
398
407
  ? `${gatewayPort}`
399
- : port.hostPort;
408
+ : port.hostPort === "{{ assistantPort }}"
409
+ ? `${assistantPort}`
410
+ : port.hostPort;
400
411
  if (hostSide !== undefined) {
401
412
  args.push("-p", `${hostSide}:${port.containerPort}`);
402
413
  }
@@ -13,6 +13,7 @@ import {
13
13
  DOCKER_READY_TIMEOUT_MS,
14
14
  dockerResourceNames,
15
15
  GATEWAY_INTERNAL_PORT,
16
+ ASSISTANT_INTERNAL_PORT,
16
17
  startContainers,
17
18
  stopContainers,
18
19
  } from "./docker.js";
@@ -685,6 +686,9 @@ export async function performDockerRollback(
685
686
  // use default
686
687
  }
687
688
 
689
+ // Recover the assistant host port from the entry, fall back to default.
690
+ const assistantPort = entry.containerInfo?.assistantPort ?? ASSISTANT_INTERNAL_PORT;
691
+
688
692
  // Broadcast SSE "starting" event
689
693
  console.log("📢 Notifying connected clients...");
690
694
  await broadcastUpgradeEvent(
@@ -746,6 +750,7 @@ export async function performDockerRollback(
746
750
  extraAssistantEnv,
747
751
  extraGatewayEnv,
748
752
  gatewayPort,
753
+ assistantPort,
749
754
  imageTags: targetImageTags,
750
755
  instanceName,
751
756
  res,
@@ -785,6 +790,7 @@ export async function performDockerRollback(
785
790
  gatewayDigest: newDigests?.gateway,
786
791
  cesDigest: newDigests?.["credential-executor"],
787
792
  networkName: res.network,
793
+ assistantPort,
788
794
  },
789
795
  previousContainerInfo: entry.containerInfo,
790
796
  previousDbMigrationVersion: preMigrationState.dbVersion,
@@ -867,6 +873,7 @@ export async function performDockerRollback(
867
873
  extraAssistantEnv,
868
874
  extraGatewayEnv,
869
875
  gatewayPort,
876
+ assistantPort,
870
877
  imageTags: currentImageRefs,
871
878
  instanceName,
872
879
  res,
@@ -919,6 +926,7 @@ export async function performDockerRollback(
919
926
  revertDigests?.["credential-executor"] ??
920
927
  currentImageRefs["credential-executor"],
921
928
  networkName: res.network,
929
+ assistantPort,
922
930
  },
923
931
  previousContainerInfo: undefined,
924
932
  previousDbMigrationVersion: undefined,