@vellumai/cli 0.7.1 → 0.7.2
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/AGENTS.md +3 -11
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +121 -5
- package/src/__tests__/teleport.test.ts +512 -10
- package/src/commands/backup.ts +35 -2
- package/src/commands/client.ts +47 -6
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +13 -4
- package/src/commands/hatch.ts +1 -1
- package/src/commands/restore.ts +7 -1
- package/src/commands/setup.ts +38 -73
- package/src/commands/teleport.ts +111 -11
- package/src/commands/upgrade.ts +6 -0
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +40 -126
- package/src/index.ts +1 -7
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +186 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +235 -0
- package/src/lib/__tests__/runtime-url.test.ts +39 -1
- package/src/lib/assistant-client.ts +13 -5
- package/src/lib/assistant-config.ts +0 -25
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/docker-statefulset.ts +381 -0
- package/src/lib/docker.ts +8 -247
- package/src/lib/guardian-token.ts +56 -6
- package/src/lib/hatch-local.ts +3 -26
- package/src/lib/local-runtime-client.ts +82 -1
- package/src/lib/local.ts +9 -7
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +96 -1
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +22 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/commands/pair.ts +0 -212
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative StatefulSet spec for the Docker service group.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the schema of the platform's `stateful_template.yaml` (vembda).
|
|
5
|
+
* Container names, volume names, and env var names are kept in sync with
|
|
6
|
+
* the K8s template so the two topologies are auditable side-by-side.
|
|
7
|
+
*
|
|
8
|
+
* This file is self-contained — it does not import from `docker.ts` to
|
|
9
|
+
* avoid a circular dependency. Constants are inlined from their definitions
|
|
10
|
+
* in `docker.ts` and must be kept in sync if those change.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
|
|
15
|
+
import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants (mirrored from docker.ts — keep in sync)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const GATEWAY_INTERNAL_PORT = 7830;
|
|
22
|
+
const ASSISTANT_INTERNAL_PORT = 7821;
|
|
23
|
+
const AVATAR_DEVICE_ENV_VAR = "VELLUM_AVATAR_DEVICE";
|
|
24
|
+
|
|
25
|
+
/** Logical service name used throughout the CLI. */
|
|
26
|
+
export type ServiceName = "assistant" | "gateway" | "credential-executor";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
/**
|
|
30
|
+
* The four fields from `dockerResourceNames()` that the builder actually uses.
|
|
31
|
+
* Container/network names come from here; volume names are generated by the
|
|
32
|
+
* `volumeClaimTemplates` in the spec. `ReturnType<typeof dockerResourceNames>`
|
|
33
|
+
* structurally satisfies this interface (it has all these fields plus more).
|
|
34
|
+
*/
|
|
35
|
+
export interface DockerResourceNames {
|
|
36
|
+
assistantContainer: string;
|
|
37
|
+
cesContainer: string;
|
|
38
|
+
gatewayContainer: string;
|
|
39
|
+
network: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Types
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** A static env var whose value is known at spec-definition time. */
|
|
47
|
+
interface StaticEnv {
|
|
48
|
+
kind: "static";
|
|
49
|
+
name: string;
|
|
50
|
+
value: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An env var whose value comes from the opts object passed to
|
|
55
|
+
* `buildServiceRunArgs` at container-start time.
|
|
56
|
+
*/
|
|
57
|
+
interface SecretEnv {
|
|
58
|
+
kind: "secret";
|
|
59
|
+
name: string;
|
|
60
|
+
/** Key in `DockerRunSecrets`. */
|
|
61
|
+
secret: keyof DockerRunSecrets;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* An env var conditionally forwarded from the host process environment.
|
|
66
|
+
* Only emitted when `process.env[hostVar]` is set.
|
|
67
|
+
* Defaults to `name` for `hostVar` when omitted.
|
|
68
|
+
*/
|
|
69
|
+
interface HostEnv {
|
|
70
|
+
kind: "host";
|
|
71
|
+
name: string;
|
|
72
|
+
hostVar?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type EnvEntry = StaticEnv | SecretEnv | HostEnv;
|
|
76
|
+
|
|
77
|
+
interface VolumeMount {
|
|
78
|
+
/** Logical volume name — maps to a Docker volume via `DockerVolumeClaimTemplate`. */
|
|
79
|
+
volumeName: string;
|
|
80
|
+
mountPath: string;
|
|
81
|
+
readOnly?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface PortSpec {
|
|
85
|
+
containerPort: number;
|
|
86
|
+
/**
|
|
87
|
+
* Host-side port. Literal string = use as-is. `"{{ gatewayPort }}"` is
|
|
88
|
+
* a sentinel replaced with the instance-specific gateway port at build time.
|
|
89
|
+
*/
|
|
90
|
+
hostPort?: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DockerContainerSpec {
|
|
95
|
+
/** K8s-style container name (matches `stateful_template.yaml`). */
|
|
96
|
+
name: string;
|
|
97
|
+
/** Internal CLI `ServiceName` key used by `SERVICE_START_ORDER`. */
|
|
98
|
+
internalName: ServiceName;
|
|
99
|
+
/**
|
|
100
|
+
* Network mode:
|
|
101
|
+
* - "bridge": owns the network (assistant only — gateway port published here).
|
|
102
|
+
* - "container": share the assistant container's network namespace.
|
|
103
|
+
*/
|
|
104
|
+
network: "bridge" | "container";
|
|
105
|
+
ports?: PortSpec[];
|
|
106
|
+
env: EnvEntry[];
|
|
107
|
+
volumeMounts: VolumeMount[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DockerVolumeClaimTemplate {
|
|
111
|
+
name: string;
|
|
112
|
+
dockerVolume: (instanceName: string) => string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DockerStatefulSetSpec {
|
|
116
|
+
containers: DockerContainerSpec[];
|
|
117
|
+
startOrder: ServiceName[];
|
|
118
|
+
volumeClaimTemplates: DockerVolumeClaimTemplate[];
|
|
119
|
+
readiness: {
|
|
120
|
+
endpoint: string;
|
|
121
|
+
timeoutMs: number;
|
|
122
|
+
intervalMs: number;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Secrets injected at container-start time (from hatch / upgrade opts). */
|
|
127
|
+
export interface DockerRunSecrets {
|
|
128
|
+
signingKey?: string;
|
|
129
|
+
bootstrapSecret?: string;
|
|
130
|
+
cesServiceToken?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Spec
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export const DOCKER_STATEFUL_SET_SPEC: DockerStatefulSetSpec = {
|
|
138
|
+
startOrder: ["assistant", "gateway", "credential-executor"],
|
|
139
|
+
|
|
140
|
+
readiness: {
|
|
141
|
+
endpoint: "/readyz",
|
|
142
|
+
timeoutMs: 180_000,
|
|
143
|
+
intervalMs: 1_000,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
volumeClaimTemplates: [
|
|
147
|
+
{ name: "assistant-workspace", dockerVolume: (n) => `${n}-workspace` },
|
|
148
|
+
{ name: "ces-bootstrap-socket", dockerVolume: (n) => `${n}-socket` },
|
|
149
|
+
{ name: "assistant-ipc-socket", dockerVolume: (n) => `${n}-assistant-ipc` },
|
|
150
|
+
{ name: "gateway-ipc-socket", dockerVolume: (n) => `${n}-gateway-ipc` },
|
|
151
|
+
{ name: "gateway-security", dockerVolume: (n) => `${n}-gateway-sec` },
|
|
152
|
+
{ name: "ces-security", dockerVolume: (n) => `${n}-ces-sec` },
|
|
153
|
+
],
|
|
154
|
+
|
|
155
|
+
containers: [
|
|
156
|
+
{
|
|
157
|
+
name: "assistant-container",
|
|
158
|
+
internalName: "assistant",
|
|
159
|
+
network: "bridge",
|
|
160
|
+
ports: [
|
|
161
|
+
{
|
|
162
|
+
containerPort: GATEWAY_INTERNAL_PORT,
|
|
163
|
+
hostPort: "{{ gatewayPort }}",
|
|
164
|
+
description: "Gateway reverse-proxy (published to host)",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
containerPort: ASSISTANT_INTERNAL_PORT,
|
|
168
|
+
hostPort: `${ASSISTANT_INTERNAL_PORT}`,
|
|
169
|
+
description: "Assistant HTTP API",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
env: [
|
|
173
|
+
{ kind: "static", name: "IS_CONTAINERIZED", value: "true" },
|
|
174
|
+
{ kind: "static", name: "DEBUG_STDOUT_LOGS", value: "1" },
|
|
175
|
+
{ kind: "static", name: "VELLUM_CLOUD", value: "docker" },
|
|
176
|
+
{ kind: "static", name: "RUNTIME_HTTP_HOST", value: "0.0.0.0" },
|
|
177
|
+
{ kind: "static", name: "VELLUM_WORKSPACE_DIR", value: "/workspace" },
|
|
178
|
+
{ kind: "static", name: "VELLUM_BACKUP_DIR", value: "/workspace/.backups" },
|
|
179
|
+
{ kind: "static", name: "VELLUM_BACKUP_KEY_PATH", value: "/workspace/.backup.key" },
|
|
180
|
+
{ kind: "static", name: "CES_CREDENTIAL_URL", value: "http://localhost:8090" },
|
|
181
|
+
{ kind: "static", name: "GATEWAY_IPC_SOCKET_DIR", value: "/run/gateway-ipc" },
|
|
182
|
+
{ kind: "static", name: "ASSISTANT_IPC_SOCKET_DIR", value: "/run/assistant-ipc" },
|
|
183
|
+
{ kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
|
|
184
|
+
{ kind: "secret", name: "ACTOR_TOKEN_SIGNING_KEY", secret: "signingKey" },
|
|
185
|
+
{ kind: "secret", name: "GUARDIAN_BOOTSTRAP_SECRET", secret: "bootstrapSecret" },
|
|
186
|
+
// Provider API keys — forwarded from host if set
|
|
187
|
+
...Object.values(PROVIDER_ENV_VAR_NAMES).map(
|
|
188
|
+
(v): HostEnv => ({ kind: "host", name: v }),
|
|
189
|
+
),
|
|
190
|
+
{ kind: "host", name: "VELLUM_ENVIRONMENT" },
|
|
191
|
+
{ kind: "host", name: "VELLUM_PLATFORM_URL" },
|
|
192
|
+
],
|
|
193
|
+
volumeMounts: [
|
|
194
|
+
{ volumeName: "assistant-workspace", mountPath: "/workspace" },
|
|
195
|
+
{ volumeName: "ces-bootstrap-socket", mountPath: "/run/ces-bootstrap" },
|
|
196
|
+
{ volumeName: "assistant-ipc-socket", mountPath: "/run/assistant-ipc" },
|
|
197
|
+
{ volumeName: "gateway-ipc-socket", mountPath: "/run/gateway-ipc" },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
name: "gateway-sidecar",
|
|
203
|
+
internalName: "gateway",
|
|
204
|
+
network: "container",
|
|
205
|
+
env: [
|
|
206
|
+
{ kind: "static", name: "VELLUM_WORKSPACE_DIR", value: "/workspace" },
|
|
207
|
+
{ kind: "static", name: "GATEWAY_SECURITY_DIR", value: "/gateway-security" },
|
|
208
|
+
{ kind: "static", name: "ASSISTANT_HOST", value: "localhost" },
|
|
209
|
+
{ kind: "static", name: "CES_CREDENTIAL_URL", value: "http://localhost:8090" },
|
|
210
|
+
{ kind: "static", name: "GATEWAY_IPC_SOCKET_DIR", value: "/run/gateway-ipc" },
|
|
211
|
+
{ kind: "static", name: "ASSISTANT_IPC_SOCKET_DIR", value: "/run/assistant-ipc" },
|
|
212
|
+
{ kind: "static", name: "GATEWAY_PORT", value: `${GATEWAY_INTERNAL_PORT}` },
|
|
213
|
+
{ kind: "static", name: "RUNTIME_HTTP_PORT", value: `${ASSISTANT_INTERNAL_PORT}` },
|
|
214
|
+
{ kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
|
|
215
|
+
{ kind: "secret", name: "ACTOR_TOKEN_SIGNING_KEY", secret: "signingKey" },
|
|
216
|
+
{ kind: "secret", name: "GUARDIAN_BOOTSTRAP_SECRET", secret: "bootstrapSecret" },
|
|
217
|
+
{ kind: "host", name: "VELLUM_ENVIRONMENT" },
|
|
218
|
+
{ kind: "host", name: "VELLUM_PLATFORM_URL" },
|
|
219
|
+
{ kind: "host", name: "VELAY_BASE_URL" },
|
|
220
|
+
],
|
|
221
|
+
volumeMounts: [
|
|
222
|
+
{ volumeName: "assistant-workspace", mountPath: "/workspace" },
|
|
223
|
+
{ volumeName: "gateway-security", mountPath: "/gateway-security" },
|
|
224
|
+
{ volumeName: "assistant-ipc-socket", mountPath: "/run/assistant-ipc" },
|
|
225
|
+
{ volumeName: "gateway-ipc-socket", mountPath: "/run/gateway-ipc" },
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
name: "credential-executor-sidecar",
|
|
231
|
+
internalName: "credential-executor",
|
|
232
|
+
network: "container",
|
|
233
|
+
env: [
|
|
234
|
+
{ kind: "static", name: "CES_MODE", value: "managed" },
|
|
235
|
+
{ kind: "static", name: "VELLUM_WORKSPACE_DIR", value: "/workspace" },
|
|
236
|
+
{ kind: "static", name: "CES_BOOTSTRAP_SOCKET_DIR", value: "/run/ces-bootstrap" },
|
|
237
|
+
{ kind: "static", name: "CREDENTIAL_SECURITY_DIR", value: "/ces-security" },
|
|
238
|
+
{ kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
|
|
239
|
+
],
|
|
240
|
+
volumeMounts: [
|
|
241
|
+
{ volumeName: "assistant-workspace", mountPath: "/workspace", readOnly: true },
|
|
242
|
+
{ volumeName: "ces-bootstrap-socket", mountPath: "/run/ces-bootstrap" },
|
|
243
|
+
{ volumeName: "ces-security", mountPath: "/ces-security" },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Builder
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
export interface BuildServiceRunArgsOpts extends DockerRunSecrets {
|
|
254
|
+
gatewayPort: number;
|
|
255
|
+
imageTags: Record<ServiceName, string>;
|
|
256
|
+
instanceName: string;
|
|
257
|
+
res: DockerResourceNames;
|
|
258
|
+
extraAssistantEnv?: Record<string, string>;
|
|
259
|
+
defaultWorkspaceConfigPath?: string;
|
|
260
|
+
/** Avatar device path, if available. Injected by `docker.ts` after resolving. */
|
|
261
|
+
avatarDevicePath?: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveVolume(
|
|
265
|
+
spec: DockerStatefulSetSpec,
|
|
266
|
+
instanceName: string,
|
|
267
|
+
volumeName: string,
|
|
268
|
+
): string {
|
|
269
|
+
const claim = spec.volumeClaimTemplates.find((v) => v.name === volumeName);
|
|
270
|
+
if (!claim) throw new Error(`docker-statefulset: unknown volume "${volumeName}"`);
|
|
271
|
+
return claim.dockerVolume(instanceName);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build `docker run` argument arrays for each container in the StatefulSet.
|
|
276
|
+
* Returns `Record<ServiceName, () => string[]>` — callers evaluate lazily.
|
|
277
|
+
*/
|
|
278
|
+
export function buildServiceRunArgs(
|
|
279
|
+
opts: BuildServiceRunArgsOpts,
|
|
280
|
+
spec = DOCKER_STATEFUL_SET_SPEC,
|
|
281
|
+
): Record<ServiceName, () => string[]> {
|
|
282
|
+
const {
|
|
283
|
+
gatewayPort,
|
|
284
|
+
imageTags,
|
|
285
|
+
instanceName,
|
|
286
|
+
res,
|
|
287
|
+
extraAssistantEnv,
|
|
288
|
+
defaultWorkspaceConfigPath,
|
|
289
|
+
avatarDevicePath,
|
|
290
|
+
} = opts;
|
|
291
|
+
|
|
292
|
+
const result = {} as Record<ServiceName, () => string[]>;
|
|
293
|
+
|
|
294
|
+
for (const container of spec.containers) {
|
|
295
|
+
const svc = container.internalName;
|
|
296
|
+
|
|
297
|
+
result[svc] = () => {
|
|
298
|
+
const args: string[] = ["run", "--init", "-d"];
|
|
299
|
+
|
|
300
|
+
// Container name
|
|
301
|
+
const containerName =
|
|
302
|
+
svc === "assistant"
|
|
303
|
+
? res.assistantContainer
|
|
304
|
+
: svc === "gateway"
|
|
305
|
+
? res.gatewayContainer
|
|
306
|
+
: res.cesContainer;
|
|
307
|
+
args.push("--name", containerName);
|
|
308
|
+
|
|
309
|
+
// Network
|
|
310
|
+
if (container.network === "bridge") {
|
|
311
|
+
args.push(`--network=${res.network}`);
|
|
312
|
+
for (const port of container.ports ?? []) {
|
|
313
|
+
const hostSide =
|
|
314
|
+
port.hostPort === "{{ gatewayPort }}"
|
|
315
|
+
? `${gatewayPort}`
|
|
316
|
+
: port.hostPort;
|
|
317
|
+
if (hostSide !== undefined) {
|
|
318
|
+
args.push("-p", `${hostSide}:${port.containerPort}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
args.push(`--network=container:${res.assistantContainer}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Volume mounts
|
|
326
|
+
for (const mount of container.volumeMounts) {
|
|
327
|
+
const vol = resolveVolume(spec, instanceName, mount.volumeName);
|
|
328
|
+
args.push("-v", mount.readOnly ? `${vol}:${mount.mountPath}:ro` : `${vol}:${mount.mountPath}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Env vars from spec
|
|
332
|
+
for (const entry of container.env) {
|
|
333
|
+
if (entry.kind === "static") {
|
|
334
|
+
args.push("-e", `${entry.name}=${entry.value}`);
|
|
335
|
+
} else if (entry.kind === "secret") {
|
|
336
|
+
const val = opts[entry.secret];
|
|
337
|
+
if (val) args.push("-e", `${entry.name}=${val}`);
|
|
338
|
+
} else {
|
|
339
|
+
const hostVar = entry.hostVar ?? entry.name;
|
|
340
|
+
const val = process.env[hostVar];
|
|
341
|
+
if (val) args.push("-e", `${entry.name}=${val}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Assistant-only computed / optional additions
|
|
346
|
+
if (svc === "assistant") {
|
|
347
|
+
args.push(
|
|
348
|
+
"-e", `VELLUM_ASSISTANT_NAME=${instanceName}`,
|
|
349
|
+
"-e", `GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (defaultWorkspaceConfigPath) {
|
|
353
|
+
const cPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
|
|
354
|
+
args.push(
|
|
355
|
+
"-v", `${defaultWorkspaceConfigPath}:${cPath}:ro`,
|
|
356
|
+
"-e", `VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH=${cPath}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (extraAssistantEnv) {
|
|
361
|
+
for (const [k, v] of Object.entries(extraAssistantEnv)) {
|
|
362
|
+
args.push("-e", `${k}=${v}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (avatarDevicePath && existsSync(avatarDevicePath)) {
|
|
367
|
+
args.push(
|
|
368
|
+
"--device", `${avatarDevicePath}:${avatarDevicePath}`,
|
|
369
|
+
"-e", `${AVATAR_DEVICE_ENV_VAR}=${avatarDevicePath}`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Image is always last
|
|
375
|
+
args.push(imageTags[svc]);
|
|
376
|
+
return args;
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return result;
|
|
381
|
+
}
|
package/src/lib/docker.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./assistant-config";
|
|
14
14
|
import type { AssistantEntry } from "./assistant-config";
|
|
15
15
|
import { writeInitialConfig } from "./config-utils";
|
|
16
|
-
import {
|
|
16
|
+
import { buildServiceRunArgs } from "./docker-statefulset.js";
|
|
17
17
|
import type { Species } from "./constants";
|
|
18
18
|
import { getDefaultPorts } from "./environments/paths.js";
|
|
19
19
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
@@ -363,7 +363,6 @@ export function dockerResourceNames(instanceName: string) {
|
|
|
363
363
|
assistantIpcVolume: `${instanceName}-assistant-ipc`,
|
|
364
364
|
cesContainer: `${instanceName}-credential-executor`,
|
|
365
365
|
cesSecurityVolume: `${instanceName}-ces-sec`,
|
|
366
|
-
dockerdDataVolume: `${instanceName}-dockerd-data`,
|
|
367
366
|
gatewayContainer: `${instanceName}-gateway`,
|
|
368
367
|
gatewayIpcVolume: `${instanceName}-gateway-ipc`,
|
|
369
368
|
gatewaySecurityVolume: `${instanceName}-gateway-sec`,
|
|
@@ -426,7 +425,6 @@ export async function retireDocker(name: string): Promise<void> {
|
|
|
426
425
|
res.workspaceVolume,
|
|
427
426
|
res.cesSecurityVolume,
|
|
428
427
|
res.gatewaySecurityVolume,
|
|
429
|
-
res.dockerdDataVolume,
|
|
430
428
|
]) {
|
|
431
429
|
try {
|
|
432
430
|
await exec("docker", ["volume", "rm", vol]);
|
|
@@ -563,10 +561,11 @@ async function buildAllImages(
|
|
|
563
561
|
}
|
|
564
562
|
|
|
565
563
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
564
|
+
* Build `docker run` argument arrays for each service in the StatefulSet.
|
|
565
|
+
*
|
|
566
|
+
* Delegates to `buildServiceRunArgs` from `docker-statefulset.ts`, which owns
|
|
567
|
+
* the declarative container / volume / env spec. Signature preserved for
|
|
568
|
+
* backward compatibility with callers throughout this file.
|
|
570
569
|
*/
|
|
571
570
|
export function serviceDockerRunArgs(opts: {
|
|
572
571
|
signingKey?: string;
|
|
@@ -579,235 +578,8 @@ export function serviceDockerRunArgs(opts: {
|
|
|
579
578
|
instanceName: string;
|
|
580
579
|
res: ReturnType<typeof dockerResourceNames>;
|
|
581
580
|
}): Record<ServiceName, () => string[]> {
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
defaultWorkspaceConfigPath,
|
|
585
|
-
extraAssistantEnv,
|
|
586
|
-
gatewayPort,
|
|
587
|
-
imageTags,
|
|
588
|
-
instanceName,
|
|
589
|
-
res,
|
|
590
|
-
} = opts;
|
|
591
|
-
return {
|
|
592
|
-
assistant: () => {
|
|
593
|
-
// Run the assistant container in Docker-in-Docker (DinD) mode: the
|
|
594
|
-
// container runs its own `dockerd` so the Meet subsystem can spawn
|
|
595
|
-
// sibling meet-bot containers without needing access to the host's
|
|
596
|
-
// Docker engine. This requires:
|
|
597
|
-
// - `CAP_SYS_ADMIN` + `CAP_NET_ADMIN` so the inner dockerd can
|
|
598
|
-
// configure cgroups, overlay mounts, network namespaces, and
|
|
599
|
-
// iptables. We deliberately avoid `--privileged` (which grants the
|
|
600
|
-
// full host capability set and access to every host device node)
|
|
601
|
-
// to shrink the escape surface from any code running inside the
|
|
602
|
-
// assistant container. See the "Security tradeoff for Docker mode"
|
|
603
|
-
// note in AGENTS.md.
|
|
604
|
-
// - `seccomp=unconfined` + `apparmor=unconfined` because Docker's
|
|
605
|
-
// default seccomp profile blocks syscalls dockerd needs (e.g.
|
|
606
|
-
// certain clone/unshare and pivot_root flags) and the default
|
|
607
|
-
// AppArmor profile on Debian/Ubuntu hosts denies the mount
|
|
608
|
-
// operations dockerd performs while launching bot containers. On
|
|
609
|
-
// hosts where these LSMs are inactive, the options are no-ops.
|
|
610
|
-
// - A dedicated named volume mounted at `/var/lib/docker` so the
|
|
611
|
-
// inner Docker image cache and container state survive restarts of
|
|
612
|
-
// the assistant container.
|
|
613
|
-
// The host's `/var/run/docker.sock` is intentionally NOT mounted — all
|
|
614
|
-
// Meet-bot spawning happens against the inner dockerd.
|
|
615
|
-
const args: string[] = [
|
|
616
|
-
"run",
|
|
617
|
-
"--init",
|
|
618
|
-
"-d",
|
|
619
|
-
"--cap-add",
|
|
620
|
-
"SYS_ADMIN",
|
|
621
|
-
"--cap-add",
|
|
622
|
-
"NET_ADMIN",
|
|
623
|
-
"--security-opt",
|
|
624
|
-
"seccomp=unconfined",
|
|
625
|
-
"--security-opt",
|
|
626
|
-
"apparmor=unconfined",
|
|
627
|
-
"--name",
|
|
628
|
-
res.assistantContainer,
|
|
629
|
-
`--network=${res.network}`,
|
|
630
|
-
"-p",
|
|
631
|
-
`${gatewayPort}:${GATEWAY_INTERNAL_PORT}`,
|
|
632
|
-
// Published so the Meet subsystem's sibling bot containers can reach
|
|
633
|
-
// the daemon's internal HTTP API at host.docker.internal:<port>.
|
|
634
|
-
//
|
|
635
|
-
// Published on all host interfaces (no `127.0.0.1:` prefix) because on
|
|
636
|
-
// vanilla Linux Docker, `host.docker.internal:host-gateway` resolves
|
|
637
|
-
// to the Docker bridge gateway IP (e.g. 172.17.0.1), not loopback.
|
|
638
|
-
// Packets from sibling containers arrive at the host's bridge
|
|
639
|
-
// interface, and an iptables DNAT rule keyed on dest=127.0.0.1 would
|
|
640
|
-
// not match — causing connection refused. Docker Desktop (macOS/
|
|
641
|
-
// Windows) still works because its VM proxy forwards to the same
|
|
642
|
-
// published port regardless of the binding address.
|
|
643
|
-
//
|
|
644
|
-
// Security tradeoff: the daemon HTTP API is now reachable from the
|
|
645
|
-
// host's LAN (any device that can hit the host IP on this port).
|
|
646
|
-
// This matches the gateway port's existing posture and is acceptable
|
|
647
|
-
// for single-user self-hosted Docker mode per the Phase 1.8 security
|
|
648
|
-
// note. Managed/multi-tenant deployments are out of scope and would
|
|
649
|
-
// require a different design.
|
|
650
|
-
"-p",
|
|
651
|
-
`${ASSISTANT_INTERNAL_PORT}:${ASSISTANT_INTERNAL_PORT}`,
|
|
652
|
-
"-v",
|
|
653
|
-
`${res.workspaceVolume}:/workspace`,
|
|
654
|
-
"-v",
|
|
655
|
-
`${res.socketVolume}:/run/ces-bootstrap`,
|
|
656
|
-
"-v",
|
|
657
|
-
`${res.assistantIpcVolume}:/run/assistant-ipc`,
|
|
658
|
-
"-v",
|
|
659
|
-
`${res.gatewayIpcVolume}:/run/gateway-ipc`,
|
|
660
|
-
"-v",
|
|
661
|
-
`${res.dockerdDataVolume}:/var/lib/docker`,
|
|
662
|
-
"-e",
|
|
663
|
-
"IS_CONTAINERIZED=true",
|
|
664
|
-
"-e",
|
|
665
|
-
"DEBUG_STDOUT_LOGS=1",
|
|
666
|
-
"-e",
|
|
667
|
-
`VELLUM_ASSISTANT_NAME=${instanceName}`,
|
|
668
|
-
"-e",
|
|
669
|
-
"VELLUM_CLOUD=docker",
|
|
670
|
-
"-e",
|
|
671
|
-
"RUNTIME_HTTP_HOST=0.0.0.0",
|
|
672
|
-
"-e",
|
|
673
|
-
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
674
|
-
"-e",
|
|
675
|
-
"VELLUM_BACKUP_DIR=/workspace/.backups",
|
|
676
|
-
"-e",
|
|
677
|
-
"VELLUM_BACKUP_KEY_PATH=/workspace/.backup.key",
|
|
678
|
-
"-e",
|
|
679
|
-
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
680
|
-
"-e",
|
|
681
|
-
`GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
|
|
682
|
-
"-e",
|
|
683
|
-
"GATEWAY_IPC_SOCKET_DIR=/run/gateway-ipc",
|
|
684
|
-
"-e",
|
|
685
|
-
"ASSISTANT_IPC_SOCKET_DIR=/run/assistant-ipc",
|
|
686
|
-
];
|
|
687
|
-
if (defaultWorkspaceConfigPath) {
|
|
688
|
-
const containerPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
|
|
689
|
-
args.push(
|
|
690
|
-
"-v",
|
|
691
|
-
`${defaultWorkspaceConfigPath}:${containerPath}:ro`,
|
|
692
|
-
"-e",
|
|
693
|
-
`VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH=${containerPath}`,
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
if (cesServiceToken) {
|
|
697
|
-
args.push("-e", `CES_SERVICE_TOKEN=${cesServiceToken}`);
|
|
698
|
-
}
|
|
699
|
-
if (opts.signingKey) {
|
|
700
|
-
args.push("-e", `ACTOR_TOKEN_SIGNING_KEY=${opts.signingKey}`);
|
|
701
|
-
}
|
|
702
|
-
if (opts.bootstrapSecret) {
|
|
703
|
-
// Mirror the secret into the assistant container so the runtime's
|
|
704
|
-
// guardian-bootstrap handler can validate the x-bootstrap-secret
|
|
705
|
-
// header forwarded by the gateway. Without this, the published
|
|
706
|
-
// runtime port would expose an unauthenticated token-minting
|
|
707
|
-
// endpoint reachable from the host bypassing the gateway's gate.
|
|
708
|
-
args.push("-e", `GUARDIAN_BOOTSTRAP_SECRET=${opts.bootstrapSecret}`);
|
|
709
|
-
}
|
|
710
|
-
for (const envVar of [
|
|
711
|
-
...Object.values(PROVIDER_ENV_VAR_NAMES),
|
|
712
|
-
"VELLUM_ENVIRONMENT",
|
|
713
|
-
"VELLUM_PLATFORM_URL",
|
|
714
|
-
]) {
|
|
715
|
-
if (process.env[envVar]) {
|
|
716
|
-
args.push("-e", `${envVar}=${process.env[envVar]}`);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
if (extraAssistantEnv) {
|
|
720
|
-
for (const [key, value] of Object.entries(extraAssistantEnv)) {
|
|
721
|
-
args.push("-e", `${key}=${value}`);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
const avatarDevice = resolveAvatarDevicePath();
|
|
725
|
-
if (existsSync(avatarDevice)) {
|
|
726
|
-
args.push(
|
|
727
|
-
"--device",
|
|
728
|
-
`${avatarDevice}:${avatarDevice}`,
|
|
729
|
-
"-e",
|
|
730
|
-
`${AVATAR_DEVICE_ENV_VAR}=${avatarDevice}`,
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
args.push(imageTags.assistant);
|
|
734
|
-
return args;
|
|
735
|
-
},
|
|
736
|
-
gateway: () => [
|
|
737
|
-
"run",
|
|
738
|
-
"--init",
|
|
739
|
-
"-d",
|
|
740
|
-
"--name",
|
|
741
|
-
res.gatewayContainer,
|
|
742
|
-
`--network=container:${res.assistantContainer}`,
|
|
743
|
-
"-v",
|
|
744
|
-
`${res.workspaceVolume}:/workspace`,
|
|
745
|
-
"-v",
|
|
746
|
-
`${res.gatewaySecurityVolume}:/gateway-security`,
|
|
747
|
-
"-v",
|
|
748
|
-
`${res.assistantIpcVolume}:/run/assistant-ipc`,
|
|
749
|
-
"-v",
|
|
750
|
-
`${res.gatewayIpcVolume}:/run/gateway-ipc`,
|
|
751
|
-
"-e",
|
|
752
|
-
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
753
|
-
"-e",
|
|
754
|
-
"GATEWAY_SECURITY_DIR=/gateway-security",
|
|
755
|
-
"-e",
|
|
756
|
-
`GATEWAY_PORT=${GATEWAY_INTERNAL_PORT}`,
|
|
757
|
-
"-e",
|
|
758
|
-
"ASSISTANT_HOST=localhost",
|
|
759
|
-
"-e",
|
|
760
|
-
`RUNTIME_HTTP_PORT=${ASSISTANT_INTERNAL_PORT}`,
|
|
761
|
-
"-e",
|
|
762
|
-
"CES_CREDENTIAL_URL=http://localhost:8090",
|
|
763
|
-
"-e",
|
|
764
|
-
"GATEWAY_IPC_SOCKET_DIR=/run/gateway-ipc",
|
|
765
|
-
"-e",
|
|
766
|
-
"ASSISTANT_IPC_SOCKET_DIR=/run/assistant-ipc",
|
|
767
|
-
...(cesServiceToken
|
|
768
|
-
? ["-e", `CES_SERVICE_TOKEN=${cesServiceToken}`]
|
|
769
|
-
: []),
|
|
770
|
-
...(opts.signingKey
|
|
771
|
-
? ["-e", `ACTOR_TOKEN_SIGNING_KEY=${opts.signingKey}`]
|
|
772
|
-
: []),
|
|
773
|
-
...(opts.bootstrapSecret
|
|
774
|
-
? ["-e", `GUARDIAN_BOOTSTRAP_SECRET=${opts.bootstrapSecret}`]
|
|
775
|
-
: []),
|
|
776
|
-
...(process.env.VELLUM_ENVIRONMENT
|
|
777
|
-
? ["-e", `VELLUM_ENVIRONMENT=${process.env.VELLUM_ENVIRONMENT}`]
|
|
778
|
-
: []),
|
|
779
|
-
...(process.env.VELLUM_PLATFORM_URL
|
|
780
|
-
? ["-e", `VELLUM_PLATFORM_URL=${process.env.VELLUM_PLATFORM_URL}`]
|
|
781
|
-
: []),
|
|
782
|
-
imageTags.gateway,
|
|
783
|
-
],
|
|
784
|
-
"credential-executor": () => [
|
|
785
|
-
"run",
|
|
786
|
-
"--init",
|
|
787
|
-
"-d",
|
|
788
|
-
"--name",
|
|
789
|
-
res.cesContainer,
|
|
790
|
-
`--network=container:${res.assistantContainer}`,
|
|
791
|
-
"-v",
|
|
792
|
-
`${res.socketVolume}:/run/ces-bootstrap`,
|
|
793
|
-
"-v",
|
|
794
|
-
`${res.workspaceVolume}:/workspace:ro`,
|
|
795
|
-
"-v",
|
|
796
|
-
`${res.cesSecurityVolume}:/ces-security`,
|
|
797
|
-
"-e",
|
|
798
|
-
"CES_MODE=managed",
|
|
799
|
-
"-e",
|
|
800
|
-
"VELLUM_WORKSPACE_DIR=/workspace",
|
|
801
|
-
"-e",
|
|
802
|
-
"CES_BOOTSTRAP_SOCKET_DIR=/run/ces-bootstrap",
|
|
803
|
-
"-e",
|
|
804
|
-
"CREDENTIAL_SECURITY_DIR=/ces-security",
|
|
805
|
-
...(cesServiceToken
|
|
806
|
-
? ["-e", `CES_SERVICE_TOKEN=${cesServiceToken}`]
|
|
807
|
-
: []),
|
|
808
|
-
imageTags["credential-executor"],
|
|
809
|
-
],
|
|
810
|
-
};
|
|
581
|
+
const avatarDevice = resolveAvatarDevicePath();
|
|
582
|
+
return buildServiceRunArgs({ ...opts, avatarDevicePath: avatarDevice });
|
|
811
583
|
}
|
|
812
584
|
|
|
813
585
|
/** The order in which services must be started. */
|
|
@@ -832,16 +604,6 @@ export async function startContainers(
|
|
|
832
604
|
},
|
|
833
605
|
log: (msg: string) => void,
|
|
834
606
|
): Promise<void> {
|
|
835
|
-
// Ensure the inner dockerd's data volume exists before mounting it.
|
|
836
|
-
// For instances hatched on Phase 1.10+, this is created in hatchDocker and
|
|
837
|
-
// is a no-op here. For instances that pre-date Phase 1.10 (DinD) and are
|
|
838
|
-
// upgrading in place, Docker would otherwise auto-create the volume on
|
|
839
|
-
// first `-v` mount without our standard ownership/labeling. Creating it
|
|
840
|
-
// explicitly keeps volume provenance consistent across fresh and upgraded
|
|
841
|
-
// instances. `docker volume create` is idempotent for an existing volume
|
|
842
|
-
// of the same name, so this is safe to run on every start.
|
|
843
|
-
await exec("docker", ["volume", "create", opts.res.dockerdDataVolume]);
|
|
844
|
-
|
|
845
607
|
const runArgs = serviceDockerRunArgs(opts);
|
|
846
608
|
for (const service of SERVICE_START_ORDER) {
|
|
847
609
|
log(`🚀 Starting ${service} container...`);
|
|
@@ -1254,7 +1016,6 @@ export async function hatchDocker(
|
|
|
1254
1016
|
await exec("docker", ["volume", "create", res.workspaceVolume]);
|
|
1255
1017
|
await exec("docker", ["volume", "create", res.cesSecurityVolume]);
|
|
1256
1018
|
await exec("docker", ["volume", "create", res.gatewaySecurityVolume]);
|
|
1257
|
-
await exec("docker", ["volume", "create", res.dockerdDataVolume]);
|
|
1258
1019
|
|
|
1259
1020
|
// Set volume ownership so non-root containers (UID 1001) can write.
|
|
1260
1021
|
await exec("docker", [
|