@vellumai/cli 0.6.4 → 0.6.5
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/bun.lock +17 -17
- package/package.json +18 -18
- package/src/__tests__/guardian-token.test.ts +56 -3
- package/src/__tests__/llm-provider-env-var-parity.test.ts +64 -0
- package/src/__tests__/multi-local.test.ts +30 -0
- package/src/commands/exec.ts +186 -0
- package/src/commands/login.ts +32 -1
- package/src/commands/ssh.ts +1 -1
- package/src/commands/teleport.ts +28 -1
- package/src/commands/wake.ts +11 -0
- package/src/index.ts +3 -0
- package/src/lib/__tests__/docker.test.ts +91 -1
- package/src/lib/assistant-config.ts +35 -22
- package/src/lib/config-utils.ts +4 -4
- package/src/lib/docker.ts +88 -4
- package/src/lib/environments/__tests__/paths.test.ts +3 -9
- package/src/lib/environments/__tests__/seeds.test.ts +72 -0
- package/src/lib/environments/paths.ts +4 -5
- package/src/lib/environments/seeds.ts +29 -1
- package/src/lib/exec-apple-container.ts +122 -0
- package/src/lib/guardian-token.ts +63 -0
- package/src/lib/hatch-local.ts +20 -4
- package/src/lib/local.ts +1 -0
- package/src/lib/platform-client.ts +134 -0
- package/src/{commands → lib}/ssh-apple-container.ts +8 -4
- package/src/shared/provider-env-vars.ts +30 -6
package/bun.lock
CHANGED
|
@@ -5,25 +5,25 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@vellumai/cli",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"chalk": "
|
|
9
|
-
"ink": "
|
|
10
|
-
"jsqr": "
|
|
11
|
-
"nanoid": "
|
|
12
|
-
"pngjs": "
|
|
13
|
-
"qrcode-terminal": "
|
|
14
|
-
"react": "
|
|
15
|
-
"react-devtools-core": "
|
|
8
|
+
"chalk": "5.6.2",
|
|
9
|
+
"ink": "6.8.0",
|
|
10
|
+
"jsqr": "1.4.0",
|
|
11
|
+
"nanoid": "5.1.7",
|
|
12
|
+
"pngjs": "7.0.0",
|
|
13
|
+
"qrcode-terminal": "0.12.0",
|
|
14
|
+
"react": "19.2.4",
|
|
15
|
+
"react-devtools-core": "6.1.5",
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/bun": "
|
|
19
|
-
"@types/pngjs": "
|
|
20
|
-
"@types/qrcode-terminal": "
|
|
21
|
-
"@types/react": "
|
|
22
|
-
"eslint": "
|
|
23
|
-
"knip": "
|
|
24
|
-
"prettier": "
|
|
25
|
-
"typescript": "
|
|
26
|
-
"typescript-eslint": "
|
|
18
|
+
"@types/bun": "1.3.11",
|
|
19
|
+
"@types/pngjs": "6.0.5",
|
|
20
|
+
"@types/qrcode-terminal": "0.12.2",
|
|
21
|
+
"@types/react": "19.2.14",
|
|
22
|
+
"eslint": "10.1.0",
|
|
23
|
+
"knip": "5.88.1",
|
|
24
|
+
"prettier": "3.8.1",
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"typescript-eslint": "8.58.0",
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "CLI tools for vellum-assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
"author": "Vellum AI",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"chalk": "
|
|
28
|
-
"ink": "
|
|
29
|
-
"jsqr": "
|
|
30
|
-
"nanoid": "
|
|
31
|
-
"pngjs": "
|
|
32
|
-
"qrcode-terminal": "
|
|
33
|
-
"react": "
|
|
34
|
-
"react-devtools-core": "
|
|
27
|
+
"chalk": "5.6.2",
|
|
28
|
+
"ink": "6.8.0",
|
|
29
|
+
"jsqr": "1.4.0",
|
|
30
|
+
"nanoid": "5.1.7",
|
|
31
|
+
"pngjs": "7.0.0",
|
|
32
|
+
"qrcode-terminal": "0.12.0",
|
|
33
|
+
"react": "19.2.4",
|
|
34
|
+
"react-devtools-core": "6.1.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/bun": "
|
|
38
|
-
"@types/pngjs": "
|
|
39
|
-
"@types/qrcode-terminal": "
|
|
40
|
-
"@types/react": "
|
|
41
|
-
"eslint": "
|
|
42
|
-
"knip": "
|
|
43
|
-
"prettier": "
|
|
44
|
-
"typescript": "
|
|
45
|
-
"typescript-eslint": "
|
|
37
|
+
"@types/bun": "1.3.11",
|
|
38
|
+
"@types/pngjs": "6.0.5",
|
|
39
|
+
"@types/qrcode-terminal": "0.12.2",
|
|
40
|
+
"@types/react": "19.2.14",
|
|
41
|
+
"eslint": "10.1.0",
|
|
42
|
+
"knip": "5.88.1",
|
|
43
|
+
"prettier": "3.8.1",
|
|
44
|
+
"typescript": "5.9.3",
|
|
45
|
+
"typescript-eslint": "8.58.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -7,18 +7,20 @@ import {
|
|
|
7
7
|
getOrCreatePersistedDeviceId,
|
|
8
8
|
loadGuardianToken,
|
|
9
9
|
saveGuardianToken,
|
|
10
|
+
seedGuardianTokenFromSiblingEnv,
|
|
10
11
|
type GuardianTokenData,
|
|
11
12
|
} from "../lib/guardian-token.js";
|
|
12
13
|
|
|
13
14
|
function makeTokenData(suffix: string): GuardianTokenData {
|
|
14
15
|
const now = new Date().toISOString();
|
|
16
|
+
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
15
17
|
return {
|
|
16
18
|
guardianPrincipalId: `principal-${suffix}`,
|
|
17
19
|
accessToken: `access-${suffix}`,
|
|
18
|
-
accessTokenExpiresAt:
|
|
20
|
+
accessTokenExpiresAt: oneHourFromNow,
|
|
19
21
|
refreshToken: `refresh-${suffix}`,
|
|
20
|
-
refreshTokenExpiresAt:
|
|
21
|
-
refreshAfter:
|
|
22
|
+
refreshTokenExpiresAt: oneHourFromNow,
|
|
23
|
+
refreshAfter: oneHourFromNow,
|
|
22
24
|
isNew: true,
|
|
23
25
|
deviceId: `device-${suffix}`,
|
|
24
26
|
leasedAt: now,
|
|
@@ -169,4 +171,55 @@ describe("guardian-token paths are env-scoped", () => {
|
|
|
169
171
|
const second = getOrCreatePersistedDeviceId();
|
|
170
172
|
expect(first).toBe(second);
|
|
171
173
|
});
|
|
174
|
+
|
|
175
|
+
test("seedGuardianTokenFromSiblingEnv copies a dev token into the current local env", () => {
|
|
176
|
+
// Write a token under the dev env.
|
|
177
|
+
process.env.VELLUM_ENVIRONMENT = "dev";
|
|
178
|
+
saveGuardianToken("alpha", makeTokenData("dev"));
|
|
179
|
+
|
|
180
|
+
// Switch to local env — no token present yet.
|
|
181
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
182
|
+
expect(loadGuardianToken("alpha")).toBeNull();
|
|
183
|
+
|
|
184
|
+
const seeded = seedGuardianTokenFromSiblingEnv("alpha");
|
|
185
|
+
expect(seeded).toBe(true);
|
|
186
|
+
|
|
187
|
+
const localPath = join(
|
|
188
|
+
tempHome,
|
|
189
|
+
"vellum-local",
|
|
190
|
+
"assistants",
|
|
191
|
+
"alpha",
|
|
192
|
+
"guardian-token.json",
|
|
193
|
+
);
|
|
194
|
+
expect(existsSync(localPath)).toBe(true);
|
|
195
|
+
const loaded = loadGuardianToken("alpha");
|
|
196
|
+
expect(loaded).not.toBeNull();
|
|
197
|
+
expect(loaded!.guardianPrincipalId).toBe("principal-dev");
|
|
198
|
+
|
|
199
|
+
// Idempotent — second call is a no-op.
|
|
200
|
+
expect(seedGuardianTokenFromSiblingEnv("alpha")).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("seedGuardianTokenFromSiblingEnv returns false when no sibling token exists", () => {
|
|
204
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
205
|
+
expect(seedGuardianTokenFromSiblingEnv("nonexistent")).toBe(false);
|
|
206
|
+
expect(loadGuardianToken("nonexistent")).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("seedGuardianTokenFromSiblingEnv does not overwrite an existing token", () => {
|
|
210
|
+
// Token already present in the current env.
|
|
211
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
212
|
+
saveGuardianToken("alpha", makeTokenData("local"));
|
|
213
|
+
|
|
214
|
+
// And a different sibling token in dev.
|
|
215
|
+
process.env.VELLUM_ENVIRONMENT = "dev";
|
|
216
|
+
saveGuardianToken("alpha", makeTokenData("dev"));
|
|
217
|
+
|
|
218
|
+
// Back to local — seed should no-op because a token is already present.
|
|
219
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
220
|
+
expect(seedGuardianTokenFromSiblingEnv("alpha")).toBe(false);
|
|
221
|
+
expect(loadGuardianToken("alpha")!.guardianPrincipalId).toBe(
|
|
222
|
+
"principal-local",
|
|
223
|
+
);
|
|
224
|
+
});
|
|
172
225
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
LLM_PROVIDER_ENV_VAR_NAMES,
|
|
7
|
+
SEARCH_PROVIDER_ENV_VAR_NAMES,
|
|
8
|
+
} from "../shared/provider-env-vars.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drift guard for the CLI-side LLM provider env-var mirror.
|
|
12
|
+
*
|
|
13
|
+
* `cli/src/shared/provider-env-vars.ts` hardcodes the LLM env-var names so the
|
|
14
|
+
* CLI doesn't need to import the assistant's `PROVIDER_CATALOG` (no CLI →
|
|
15
|
+
* assistant cross-package imports exist). This test pulls the catalog JSON at
|
|
16
|
+
* `meta/llm-provider-catalog.json` — which is kept in sync with
|
|
17
|
+
* `PROVIDER_CATALOG` by `assistant/src/__tests__/llm-catalog-parity.test.ts` —
|
|
18
|
+
* and asserts the CLI's mirror matches the catalog's `envVar` entries.
|
|
19
|
+
*
|
|
20
|
+
* It also asserts the search-provider mirror matches `meta/provider-env-vars.json`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = join(import.meta.dir, "..", "..", "..");
|
|
24
|
+
|
|
25
|
+
interface LlmCatalogEntry {
|
|
26
|
+
id: string;
|
|
27
|
+
envVar?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LlmCatalog {
|
|
31
|
+
version: number;
|
|
32
|
+
providers: LlmCatalogEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SearchProviderRegistry {
|
|
36
|
+
version: number;
|
|
37
|
+
providers: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadLlmCatalog(): LlmCatalog {
|
|
41
|
+
const path = join(REPO_ROOT, "meta", "llm-provider-catalog.json");
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadSearchProviderRegistry(): SearchProviderRegistry {
|
|
46
|
+
const path = join(REPO_ROOT, "meta", "provider-env-vars.json");
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("CLI provider env-var parity", () => {
|
|
51
|
+
test("LLM_PROVIDER_ENV_VAR_NAMES matches meta/llm-provider-catalog.json entries with envVar", () => {
|
|
52
|
+
const catalog = loadLlmCatalog();
|
|
53
|
+
const expected: Record<string, string> = {};
|
|
54
|
+
for (const provider of catalog.providers) {
|
|
55
|
+
if (provider.envVar) expected[provider.id] = provider.envVar;
|
|
56
|
+
}
|
|
57
|
+
expect(LLM_PROVIDER_ENV_VAR_NAMES).toEqual(expected);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("SEARCH_PROVIDER_ENV_VAR_NAMES matches meta/provider-env-vars.json", () => {
|
|
61
|
+
const registry = loadSearchProviderRegistry();
|
|
62
|
+
expect(SEARCH_PROVIDER_ENV_VAR_NAMES).toEqual(registry.providers);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -159,6 +159,36 @@ describe("multi-local", () => {
|
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
test("allocation picks env-specific port bases for non-prod envs", async () => {
|
|
163
|
+
// Each non-prod env sits in its own 1000-port window (see
|
|
164
|
+
// environments/seeds.ts). Hatching under VELLUM_ENVIRONMENT=dev should
|
|
165
|
+
// produce ports in the dev block (18000+), not the production defaults.
|
|
166
|
+
const prevEnv = process.env.VELLUM_ENVIRONMENT;
|
|
167
|
+
const prevXdg = process.env.XDG_DATA_HOME;
|
|
168
|
+
const xdgDataHome = mkdtempSync(join(tmpdir(), "cli-multi-xdg-ports-"));
|
|
169
|
+
process.env.VELLUM_ENVIRONMENT = "dev";
|
|
170
|
+
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
171
|
+
try {
|
|
172
|
+
const res = await allocateLocalResources("dev-a");
|
|
173
|
+
expect(res.daemonPort).toBe(18000);
|
|
174
|
+
expect(res.gatewayPort).toBe(18100);
|
|
175
|
+
expect(res.qdrantPort).toBe(18200);
|
|
176
|
+
expect(res.cesPort).toBe(18300);
|
|
177
|
+
} finally {
|
|
178
|
+
if (prevEnv !== undefined) {
|
|
179
|
+
process.env.VELLUM_ENVIRONMENT = prevEnv;
|
|
180
|
+
} else {
|
|
181
|
+
delete process.env.VELLUM_ENVIRONMENT;
|
|
182
|
+
}
|
|
183
|
+
if (prevXdg !== undefined) {
|
|
184
|
+
process.env.XDG_DATA_HOME = prevXdg;
|
|
185
|
+
} else {
|
|
186
|
+
delete process.env.XDG_DATA_HOME;
|
|
187
|
+
}
|
|
188
|
+
rmSync(xdgDataHome, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
162
192
|
test("second instance gets distinct ports and dir when first instance is saved", async () => {
|
|
163
193
|
// GIVEN a first local assistant already exists in the lockfile
|
|
164
194
|
saveAssistantEntry(makeEntry("instance-a"));
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findAssistantByName,
|
|
5
|
+
loadLatestAssistant,
|
|
6
|
+
resolveCloud,
|
|
7
|
+
} from "../lib/assistant-config";
|
|
8
|
+
import { dockerResourceNames } from "../lib/docker";
|
|
9
|
+
import type { ServiceName } from "../lib/docker";
|
|
10
|
+
import { execAppleContainer } from "../lib/exec-apple-container";
|
|
11
|
+
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
12
|
+
|
|
13
|
+
const SERVICE_ALIASES: Record<string, ServiceName> = {
|
|
14
|
+
assistant: "assistant",
|
|
15
|
+
"vellum-assistant": "assistant",
|
|
16
|
+
gateway: "gateway",
|
|
17
|
+
"vellum-gateway": "gateway",
|
|
18
|
+
"credential-executor": "credential-executor",
|
|
19
|
+
"vellum-credential-executor": "credential-executor",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function normalizeService(raw: string): ServiceName {
|
|
23
|
+
const normalized = SERVICE_ALIASES[raw];
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
console.error(
|
|
26
|
+
`Unknown service '${raw}'. Valid services: assistant, gateway, credential-executor`,
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveDockerContainer(
|
|
34
|
+
instanceName: string,
|
|
35
|
+
service: ServiceName,
|
|
36
|
+
): string {
|
|
37
|
+
const res = dockerResourceNames(instanceName);
|
|
38
|
+
switch (service) {
|
|
39
|
+
case "assistant":
|
|
40
|
+
return res.assistantContainer;
|
|
41
|
+
case "gateway":
|
|
42
|
+
return res.gatewayContainer;
|
|
43
|
+
case "credential-executor":
|
|
44
|
+
return res.cesContainer;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function exec(): Promise<void> {
|
|
49
|
+
const rawArgs = process.argv.slice(3);
|
|
50
|
+
|
|
51
|
+
// Only check for help flags before the -- separator so that
|
|
52
|
+
// `vellum exec -- curl --help` passes through correctly.
|
|
53
|
+
const dashDashIndex = rawArgs.indexOf("--");
|
|
54
|
+
const preArgs =
|
|
55
|
+
dashDashIndex === -1 ? rawArgs : rawArgs.slice(0, dashDashIndex);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
preArgs.includes("--help") ||
|
|
59
|
+
preArgs.includes("-h") ||
|
|
60
|
+
rawArgs.length === 0
|
|
61
|
+
) {
|
|
62
|
+
console.log(
|
|
63
|
+
"Usage: vellum exec [<name>] [--service <svc>] [-it] -- <command...>",
|
|
64
|
+
);
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log("Execute a command inside an assistant's container.");
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Arguments:");
|
|
69
|
+
console.log(
|
|
70
|
+
" <name> Name of the assistant (defaults to active)",
|
|
71
|
+
);
|
|
72
|
+
console.log(
|
|
73
|
+
" <command...> Command and arguments to run (after --)",
|
|
74
|
+
);
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log("Options:");
|
|
77
|
+
console.log(
|
|
78
|
+
" --service <svc> Target service (default: assistant)",
|
|
79
|
+
);
|
|
80
|
+
console.log(
|
|
81
|
+
" -it Interactive mode with TTY (like docker exec -it)",
|
|
82
|
+
);
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log("Services:");
|
|
85
|
+
console.log(" assistant (or vellum-assistant)");
|
|
86
|
+
console.log(" gateway (or vellum-gateway)");
|
|
87
|
+
console.log(" credential-executor (or vellum-credential-executor)");
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log("Examples:");
|
|
90
|
+
console.log(" vellum exec -- ls -la /workspace");
|
|
91
|
+
console.log(" vellum exec -- cat /workspace/NOW.md");
|
|
92
|
+
console.log(" vellum exec -it -- /bin/bash");
|
|
93
|
+
console.log(
|
|
94
|
+
" vellum exec --service gateway -- cat /tmp/gateway.log",
|
|
95
|
+
);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (dashDashIndex === -1) {
|
|
100
|
+
console.error(
|
|
101
|
+
"Error: missing '--' separator before command.\n" +
|
|
102
|
+
"Usage: vellum exec [<name>] -- <command...>",
|
|
103
|
+
);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const command = rawArgs.slice(dashDashIndex + 1);
|
|
108
|
+
|
|
109
|
+
if (command.length === 0) {
|
|
110
|
+
console.error("Error: no command specified after '--'.");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let nameArg: string | undefined;
|
|
115
|
+
let serviceRaw = "assistant";
|
|
116
|
+
let interactive = false;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < preArgs.length; i++) {
|
|
119
|
+
if (preArgs[i] === "--service" && preArgs[i + 1]) {
|
|
120
|
+
serviceRaw = preArgs[++i];
|
|
121
|
+
} else if (preArgs[i] === "-it" || preArgs[i] === "-ti") {
|
|
122
|
+
interactive = true;
|
|
123
|
+
} else if (!preArgs[i].startsWith("-")) {
|
|
124
|
+
nameArg = preArgs[i];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const service = normalizeService(serviceRaw);
|
|
129
|
+
|
|
130
|
+
const entry = nameArg
|
|
131
|
+
? findAssistantByName(nameArg)
|
|
132
|
+
: loadLatestAssistant();
|
|
133
|
+
|
|
134
|
+
if (!entry) {
|
|
135
|
+
if (nameArg) {
|
|
136
|
+
console.error(`No assistant instance found with name '${nameArg}'.`);
|
|
137
|
+
} else {
|
|
138
|
+
console.error("No assistant instance found. Run `vellum hatch` first.");
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cloud = resolveCloud(entry);
|
|
144
|
+
|
|
145
|
+
if (cloud === "local") {
|
|
146
|
+
console.error(
|
|
147
|
+
"Cannot exec into a local assistant — it runs directly on this machine.",
|
|
148
|
+
);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (cloud === "apple-container") {
|
|
153
|
+
const fullServiceName = `vellum-${service}`;
|
|
154
|
+
if (interactive) {
|
|
155
|
+
await sshAppleContainer(entry, command, fullServiceName);
|
|
156
|
+
} else {
|
|
157
|
+
await execAppleContainer(entry, command, fullServiceName);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (cloud === "docker") {
|
|
163
|
+
const container = resolveDockerContainer(entry.assistantId, service);
|
|
164
|
+
const dockerArgs = interactive
|
|
165
|
+
? ["exec", "-it", container, ...command]
|
|
166
|
+
: ["exec", container, ...command];
|
|
167
|
+
|
|
168
|
+
const child = spawn("docker", dockerArgs, { stdio: "inherit" });
|
|
169
|
+
await new Promise<void>((resolve, reject) => {
|
|
170
|
+
child.on("close", (code) => {
|
|
171
|
+
if (code === 0) resolve();
|
|
172
|
+
else {
|
|
173
|
+
process.exitCode = code ?? 1;
|
|
174
|
+
resolve();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
child.on("error", reject);
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.error(
|
|
183
|
+
`Error: 'vellum exec' is not supported for ${cloud} instances.`,
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
package/src/commands/login.ts
CHANGED
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
fetchOrganizationId,
|
|
16
16
|
getPlatformUrl,
|
|
17
17
|
injectCredentialsIntoAssistant,
|
|
18
|
+
readGatewayCredential,
|
|
18
19
|
readPlatformToken,
|
|
20
|
+
reprovisionAssistantApiKey,
|
|
19
21
|
savePlatformToken,
|
|
20
22
|
} from "../lib/platform-client";
|
|
21
23
|
|
|
@@ -193,12 +195,41 @@ export async function login(): Promise<void> {
|
|
|
193
195
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
|
194
196
|
);
|
|
195
197
|
|
|
198
|
+
// Resolve the API key to inject, mirroring the macOS app's
|
|
199
|
+
// LocalAssistantBootstrapService 3-step flow:
|
|
200
|
+
// 1. Use fresh key from registration (first-time only)
|
|
201
|
+
// 2. Use existing key from the daemon's credential store
|
|
202
|
+
// 3. Reprovision (rotate) as a last resort — this revokes the
|
|
203
|
+
// old key server-side, so we only do it when the gateway
|
|
204
|
+
// confirms no key exists (not when it's merely unreachable).
|
|
205
|
+
let assistantApiKey = registration.assistant_api_key;
|
|
206
|
+
if (!assistantApiKey) {
|
|
207
|
+
const cached = await readGatewayCredential(
|
|
208
|
+
entry.runtimeUrl,
|
|
209
|
+
"vellum:assistant_api_key",
|
|
210
|
+
entry.bearerToken,
|
|
211
|
+
);
|
|
212
|
+
if (cached.value) {
|
|
213
|
+
assistantApiKey = cached.value;
|
|
214
|
+
} else if (!cached.unreachable) {
|
|
215
|
+
console.log("No API key available locally — reprovisioning...");
|
|
216
|
+
const reprovision = await reprovisionAssistantApiKey(
|
|
217
|
+
token,
|
|
218
|
+
orgId,
|
|
219
|
+
clientInstallationId,
|
|
220
|
+
entry.assistantId,
|
|
221
|
+
"cli",
|
|
222
|
+
);
|
|
223
|
+
assistantApiKey = reprovision.provisioning.assistant_api_key;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
// Inject credentials into the running assistant via the gateway,
|
|
197
228
|
// mirroring the desktop app's LocalAssistantBootstrapService flow.
|
|
198
229
|
const allInjected = await injectCredentialsIntoAssistant({
|
|
199
230
|
gatewayUrl: entry.runtimeUrl,
|
|
200
231
|
bearerToken: entry.bearerToken,
|
|
201
|
-
assistantApiKey
|
|
232
|
+
assistantApiKey,
|
|
202
233
|
platformAssistantId: registration.assistant.id,
|
|
203
234
|
platformBaseUrl: getPlatformUrl(),
|
|
204
235
|
organizationId: orgId,
|
package/src/commands/ssh.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../lib/assistant-config";
|
|
7
7
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
8
|
import { dockerResourceNames } from "../lib/docker";
|
|
9
|
-
import { sshAppleContainer } from "
|
|
9
|
+
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
10
10
|
|
|
11
11
|
const SSH_OPTS = [
|
|
12
12
|
"-o",
|
package/src/commands/teleport.ts
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
platformImportBundleFromGcs,
|
|
28
28
|
platformPollImportStatus,
|
|
29
29
|
ensureSelfHostedLocalRegistration,
|
|
30
|
+
readGatewayCredential,
|
|
31
|
+
reprovisionAssistantApiKey,
|
|
30
32
|
injectCredentialsIntoAssistant,
|
|
31
33
|
fetchCurrentUser,
|
|
32
34
|
fetchOrganizationId,
|
|
@@ -1135,10 +1137,35 @@ async function tryInjectPlatformCredentials(
|
|
|
1135
1137
|
"cli",
|
|
1136
1138
|
);
|
|
1137
1139
|
|
|
1140
|
+
// Resolve the API key: 1) fresh from registration, 2) existing from
|
|
1141
|
+
// daemon credential store, 3) reprovision as last resort (revokes old key).
|
|
1142
|
+
// Only reprovision when the gateway confirms no key exists — not when
|
|
1143
|
+
// the gateway is merely unreachable (would revoke without injecting).
|
|
1144
|
+
let assistantApiKey = registration.assistant_api_key;
|
|
1145
|
+
if (!assistantApiKey) {
|
|
1146
|
+
const cached = await readGatewayCredential(
|
|
1147
|
+
entry.runtimeUrl,
|
|
1148
|
+
"vellum:assistant_api_key",
|
|
1149
|
+
entry.bearerToken,
|
|
1150
|
+
);
|
|
1151
|
+
if (cached.value) {
|
|
1152
|
+
assistantApiKey = cached.value;
|
|
1153
|
+
} else if (!cached.unreachable) {
|
|
1154
|
+
const reprovision = await reprovisionAssistantApiKey(
|
|
1155
|
+
token,
|
|
1156
|
+
orgId,
|
|
1157
|
+
clientInstallationId,
|
|
1158
|
+
entry.assistantId,
|
|
1159
|
+
"cli",
|
|
1160
|
+
);
|
|
1161
|
+
assistantApiKey = reprovision.provisioning.assistant_api_key;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1138
1165
|
const allInjected = await injectCredentialsIntoAssistant({
|
|
1139
1166
|
gatewayUrl: entry.runtimeUrl,
|
|
1140
1167
|
bearerToken: entry.bearerToken,
|
|
1141
|
-
assistantApiKey
|
|
1168
|
+
assistantApiKey,
|
|
1142
1169
|
platformAssistantId: registration.assistant.id,
|
|
1143
1170
|
platformBaseUrl: getPlatformUrl(),
|
|
1144
1171
|
organizationId: orgId,
|
package/src/commands/wake.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
saveAssistantEntry,
|
|
7
7
|
} from "../lib/assistant-config.js";
|
|
8
8
|
import { dockerResourceNames, wakeContainers } from "../lib/docker.js";
|
|
9
|
+
import { seedGuardianTokenFromSiblingEnv } from "../lib/guardian-token.js";
|
|
9
10
|
import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
|
|
10
11
|
import {
|
|
11
12
|
generateLocalSigningKey,
|
|
@@ -182,6 +183,16 @@ export async function wake(): Promise<void> {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
// Self-heal the guardian token when the current environment's config dir
|
|
187
|
+
// is missing it. Hatch cross-writes the lockfile across env dirs but the
|
|
188
|
+
// guardian token is only persisted under the hatch-time env, so a desktop
|
|
189
|
+
// app built under a different VELLUM_ENVIRONMENT can't find a bearer and
|
|
190
|
+
// cascades into 401 → auth-rate-limit → 429. A sibling env copy is cheap
|
|
191
|
+
// and strictly additive.
|
|
192
|
+
if (seedGuardianTokenFromSiblingEnv(entry.assistantId)) {
|
|
193
|
+
console.log(" Seeded guardian token from sibling environment.");
|
|
194
|
+
}
|
|
195
|
+
|
|
185
196
|
// Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
|
|
186
197
|
// Scope BASE_DATA_DIR to the woken instance so ngrok reads the correct
|
|
187
198
|
// instance config, then restore on any exit path.
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { backup } from "./commands/backup";
|
|
|
5
5
|
import { clean } from "./commands/clean";
|
|
6
6
|
import { client } from "./commands/client";
|
|
7
7
|
import { events } from "./commands/events";
|
|
8
|
+
import { exec } from "./commands/exec";
|
|
8
9
|
import { hatch } from "./commands/hatch";
|
|
9
10
|
import { login, logout, whoami } from "./commands/login";
|
|
10
11
|
import { logs } from "./commands/logs";
|
|
@@ -37,6 +38,7 @@ const commands = {
|
|
|
37
38
|
clean,
|
|
38
39
|
client,
|
|
39
40
|
events,
|
|
41
|
+
exec,
|
|
40
42
|
hatch,
|
|
41
43
|
login,
|
|
42
44
|
logout,
|
|
@@ -69,6 +71,7 @@ function printHelp(): void {
|
|
|
69
71
|
console.log(" clean Kill orphaned vellum processes");
|
|
70
72
|
console.log(" client Connect to a hatched assistant");
|
|
71
73
|
console.log(" events Stream events from a running assistant");
|
|
74
|
+
console.log(" exec Execute a command inside an assistant's container");
|
|
72
75
|
console.log(" hatch Create a new assistant instance");
|
|
73
76
|
console.log(" logs View logs from an assistant instance");
|
|
74
77
|
console.log(" login Log in to the Vellum platform");
|