@vellumai/cli 0.7.0 → 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/README.md +49 -0
- package/bun.lock +0 -15
- package/package.json +1 -6
- package/src/__tests__/backup.test.ts +591 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +597 -37
- package/src/commands/backup.ts +149 -70
- package/src/commands/client.ts +56 -14
- package/src/commands/events.ts +3 -0
- package/src/commands/exec.ts +34 -12
- package/src/commands/hatch.ts +3 -7
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +32 -47
- package/src/commands/setup.ts +38 -73
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +148 -34
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +114 -7
- package/src/commands/wake.ts +5 -16
- package/src/components/DefaultMainScreen.tsx +65 -129
- package/src/index.ts +2 -13
- package/src/lib/__tests__/docker.test.ts +50 -32
- package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
- package/src/lib/__tests__/runtime-url.test.ts +125 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +18 -26
- package/src/lib/assistant-config.ts +34 -41
- package/src/lib/backup-ops.ts +43 -17
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- 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/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +162 -28
- package/src/lib/local.ts +35 -64
- package/src/lib/ngrok.ts +36 -26
- package/src/lib/platform-client.ts +97 -221
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +2 -2
- package/src/lib/runtime-url.ts +52 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/upgrade-lifecycle.ts +65 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/commands/pair.ts +0 -212
package/src/lib/config-utils.ts
CHANGED
|
@@ -2,11 +2,6 @@ import { writeFileSync } from "fs";
|
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
|
|
5
|
-
const ANTHROPIC_PROVIDER = "anthropic";
|
|
6
|
-
const ANTHROPIC_DEFAULT_MODEL = "claude-opus-4-7";
|
|
7
|
-
const MAIN_AGENT_OPUS_MODEL = "claude-opus-4-7";
|
|
8
|
-
const MAIN_AGENT_OPUS_MAX_TOKENS = 32000;
|
|
9
|
-
|
|
10
5
|
/**
|
|
11
6
|
* Convert flat dot-notation key=value pairs into a nested config object.
|
|
12
7
|
*
|
|
@@ -37,20 +32,6 @@ export function buildNestedConfig(
|
|
|
37
32
|
return config;
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
/**
|
|
41
|
-
* Build the first-boot workspace config overlay passed to the assistant during
|
|
42
|
-
* hatch. Anthropic onboarding sets `llm.default.model` to Sonnet so background
|
|
43
|
-
* fallback work stays cheaper, while the main conversation thread should remain
|
|
44
|
-
* on Opus via the same call-site override seeded by workspace migration 050.
|
|
45
|
-
*/
|
|
46
|
-
export function buildInitialConfig(
|
|
47
|
-
configValues: Record<string, string>,
|
|
48
|
-
): Record<string, unknown> {
|
|
49
|
-
const config = buildNestedConfig(configValues);
|
|
50
|
-
seedAnthropicMainAgentCallSite(config);
|
|
51
|
-
return config;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
35
|
/**
|
|
55
36
|
* Write arbitrary key-value pairs to a temporary JSON file and return its
|
|
56
37
|
* path. The caller passes this path to the daemon via the
|
|
@@ -68,7 +49,7 @@ export function writeInitialConfig(
|
|
|
68
49
|
): string | undefined {
|
|
69
50
|
if (Object.keys(configValues).length === 0) return undefined;
|
|
70
51
|
|
|
71
|
-
const config =
|
|
52
|
+
const config = buildNestedConfig(configValues);
|
|
72
53
|
const tempPath = join(
|
|
73
54
|
tmpdir(),
|
|
74
55
|
`vellum-default-workspace-config-${process.pid}-${Date.now()}.json`,
|
|
@@ -76,80 +57,3 @@ export function writeInitialConfig(
|
|
|
76
57
|
writeFileSync(tempPath, JSON.stringify(config, null, 2) + "\n");
|
|
77
58
|
return tempPath;
|
|
78
59
|
}
|
|
79
|
-
|
|
80
|
-
function seedAnthropicMainAgentCallSite(config: Record<string, unknown>): void {
|
|
81
|
-
const llm = ensureObject(config, "llm");
|
|
82
|
-
|
|
83
|
-
const existingCallSites = readObject(llm.callSites);
|
|
84
|
-
if (existingCallSites !== null && "mainAgent" in existingCallSites) return;
|
|
85
|
-
|
|
86
|
-
const { provider, model } = resolveInitialMainAgentBaseSelection(llm);
|
|
87
|
-
if (provider !== ANTHROPIC_PROVIDER) return;
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
model !== undefined &&
|
|
91
|
-
model !== ANTHROPIC_DEFAULT_MODEL &&
|
|
92
|
-
model !== MAIN_AGENT_OPUS_MODEL
|
|
93
|
-
) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const callSites = ensureObject(llm, "callSites");
|
|
98
|
-
|
|
99
|
-
callSites.mainAgent = {
|
|
100
|
-
model: MAIN_AGENT_OPUS_MODEL,
|
|
101
|
-
maxTokens: MAIN_AGENT_OPUS_MAX_TOKENS,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function resolveInitialMainAgentBaseSelection(llm: Record<string, unknown>): {
|
|
106
|
-
provider: string;
|
|
107
|
-
model?: string;
|
|
108
|
-
} {
|
|
109
|
-
const defaultBlock = readObject(llm.default);
|
|
110
|
-
let provider = readString(defaultBlock?.provider) ?? ANTHROPIC_PROVIDER;
|
|
111
|
-
let model = readString(defaultBlock?.model);
|
|
112
|
-
|
|
113
|
-
const profiles = readObject(llm.profiles);
|
|
114
|
-
const activeProfileName = readString(llm.activeProfile);
|
|
115
|
-
const activeProfile =
|
|
116
|
-
profiles !== null && activeProfileName !== undefined
|
|
117
|
-
? readObject(profiles[activeProfileName])
|
|
118
|
-
: null;
|
|
119
|
-
|
|
120
|
-
if (activeProfile !== null) {
|
|
121
|
-
provider = readString(activeProfile.provider) ?? provider;
|
|
122
|
-
model = readString(activeProfile.model) ?? model;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return model === undefined ? { provider } : { provider, model };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function ensureObject(
|
|
129
|
-
parent: Record<string, unknown>,
|
|
130
|
-
key: string,
|
|
131
|
-
): Record<string, unknown> {
|
|
132
|
-
const existing = parent[key];
|
|
133
|
-
if (
|
|
134
|
-
existing != null &&
|
|
135
|
-
typeof existing === "object" &&
|
|
136
|
-
!Array.isArray(existing)
|
|
137
|
-
) {
|
|
138
|
-
return existing as Record<string, unknown>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const next: Record<string, unknown> = {};
|
|
142
|
-
parent[key] = next;
|
|
143
|
-
return next;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function readObject(value: unknown): Record<string, unknown> | null {
|
|
147
|
-
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
return value as Record<string, unknown>;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function readString(value: unknown): string | undefined {
|
|
154
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
155
|
-
}
|
|
@@ -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
|
+
}
|