@vellumai/cli 0.8.10 → 0.8.11-dev.202606112057.e4bc22e
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 +2 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +13 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +2 -2
- package/node_modules/@vellumai/local-mode/src/index.ts +1 -1
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +20 -1
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +3 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +169 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +9 -4
- package/package.json +1 -1
- package/src/__tests__/confirm.test.ts +85 -0
- package/src/__tests__/device-id.test.ts +167 -0
- package/src/__tests__/guardian-token.test.ts +79 -0
- package/src/__tests__/helpers/env.ts +19 -0
- package/src/__tests__/statefulset.test.ts +149 -0
- package/src/__tests__/upgrade-replay-env.test.ts +165 -0
- package/src/__tests__/wake.test.ts +68 -0
- package/src/commands/backup.ts +3 -2
- package/src/commands/client.ts +22 -5
- package/src/commands/confirm.ts +144 -0
- package/src/commands/connect.ts +1 -1
- package/src/commands/devices.ts +4 -3
- package/src/commands/hatch.ts +16 -1
- package/src/commands/pair.ts +3 -2
- package/src/commands/restore.ts +3 -2
- package/src/commands/retire.ts +2 -1
- package/src/commands/roadmap.ts +2 -1
- package/src/commands/rollback.ts +9 -37
- package/src/commands/unpair.ts +1 -1
- package/src/commands/upgrade.ts +13 -44
- package/src/commands/wake.ts +49 -1
- package/src/index.ts +11 -4
- package/src/lib/assistant-client.ts +3 -2
- package/src/lib/backup-ops.ts +5 -4
- package/src/lib/device-id.ts +85 -0
- package/src/lib/docker.ts +19 -3
- package/src/lib/guardian-token.ts +44 -8
- package/src/lib/hatch-local.ts +2 -1
- package/src/lib/health-check.ts +6 -4
- package/src/lib/http-client.ts +3 -1
- package/src/lib/local-runtime-client.ts +5 -4
- package/src/lib/local.ts +1 -0
- package/src/lib/loopback-fetch.ts +28 -0
- package/src/lib/ngrok.ts +2 -1
- package/src/lib/platform-client.ts +28 -21
- package/src/lib/platform-releases.ts +3 -2
- package/src/lib/statefulset.ts +43 -0
- package/src/lib/terminal-client.ts +6 -5
- package/src/lib/upgrade-lifecycle.ts +114 -53
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot the given env vars now; returns a restore function suitable for
|
|
3
|
+
* `afterEach` that resets each var to its captured value (or deletes it).
|
|
4
|
+
*/
|
|
5
|
+
export function snapshotEnv(keys: readonly string[]): () => void {
|
|
6
|
+
const saved: Record<string, string | undefined> = {};
|
|
7
|
+
for (const key of keys) {
|
|
8
|
+
saved[key] = process.env[key];
|
|
9
|
+
}
|
|
10
|
+
return () => {
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
if (saved[key] === undefined) {
|
|
13
|
+
delete process.env[key];
|
|
14
|
+
} else {
|
|
15
|
+
process.env[key] = saved[key];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildServiceRunArgs,
|
|
5
|
+
getBuilderManagedEnvKeys,
|
|
6
|
+
type BuildServiceRunArgsOpts,
|
|
7
|
+
type DockerStatefulSetSpec,
|
|
8
|
+
type ServiceName,
|
|
9
|
+
} from "../lib/statefulset.js";
|
|
10
|
+
import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
|
|
11
|
+
|
|
12
|
+
const SECRET_KEYS = [
|
|
13
|
+
"CES_SERVICE_TOKEN",
|
|
14
|
+
"ACTOR_TOKEN_SIGNING_KEY",
|
|
15
|
+
"GUARDIAN_BOOTSTRAP_SECRET",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
describe("getBuilderManagedEnvKeys", () => {
|
|
19
|
+
test("gateway always-set keys cover spec static + secret entries and PATH", () => {
|
|
20
|
+
const { always } = getBuilderManagedEnvKeys("gateway");
|
|
21
|
+
|
|
22
|
+
const expected = [
|
|
23
|
+
"VELLUM_WORKSPACE_DIR",
|
|
24
|
+
"GATEWAY_SECURITY_DIR",
|
|
25
|
+
"ASSISTANT_HOST",
|
|
26
|
+
"CES_CREDENTIAL_URL",
|
|
27
|
+
"GATEWAY_IPC_SOCKET_DIR",
|
|
28
|
+
"ASSISTANT_IPC_SOCKET_DIR",
|
|
29
|
+
"GATEWAY_PORT",
|
|
30
|
+
"RUNTIME_HTTP_PORT",
|
|
31
|
+
...SECRET_KEYS,
|
|
32
|
+
"PATH",
|
|
33
|
+
];
|
|
34
|
+
for (const key of expected) {
|
|
35
|
+
expect(always.has(key)).toBe(true);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
expect(always.has("VELLUM_DISABLE_PLATFORM")).toBe(false);
|
|
39
|
+
expect(always.has("VELLUM_DEVICE_ID")).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("assistant always-set keys include secrets and builder-computed extras", () => {
|
|
43
|
+
const { always } = getBuilderManagedEnvKeys("assistant");
|
|
44
|
+
|
|
45
|
+
const expected = [
|
|
46
|
+
...SECRET_KEYS,
|
|
47
|
+
"VELLUM_ASSISTANT_NAME",
|
|
48
|
+
"GATEWAY_INTERNAL_URL",
|
|
49
|
+
"RUNTIME_HTTP_HOST",
|
|
50
|
+
"PATH",
|
|
51
|
+
];
|
|
52
|
+
for (const key of expected) {
|
|
53
|
+
expect(always.has(key)).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("gateway hostForwarded equals the three spec host entries", () => {
|
|
58
|
+
const { hostForwarded } = getBuilderManagedEnvKeys("gateway");
|
|
59
|
+
const sorted = [...hostForwarded].sort((a, b) => a.name.localeCompare(b.name));
|
|
60
|
+
expect(sorted).toEqual([
|
|
61
|
+
{ name: "VELAY_BASE_URL", hostVar: "VELAY_BASE_URL" },
|
|
62
|
+
{ name: "VELLUM_ENVIRONMENT", hostVar: "VELLUM_ENVIRONMENT" },
|
|
63
|
+
{ name: "VELLUM_PLATFORM_URL", hostVar: "VELLUM_PLATFORM_URL" },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("assistant hostForwarded includes provider keys and platform URL", () => {
|
|
68
|
+
const { hostForwarded } = getBuilderManagedEnvKeys("assistant");
|
|
69
|
+
expect(hostForwarded).toContainEqual({
|
|
70
|
+
name: "ANTHROPIC_API_KEY",
|
|
71
|
+
hostVar: "ANTHROPIC_API_KEY",
|
|
72
|
+
});
|
|
73
|
+
for (const envVar of Object.values(PROVIDER_ENV_VAR_NAMES)) {
|
|
74
|
+
expect(hostForwarded).toContainEqual({ name: envVar, hostVar: envVar });
|
|
75
|
+
}
|
|
76
|
+
expect(hostForwarded).toContainEqual({
|
|
77
|
+
name: "VELLUM_PLATFORM_URL",
|
|
78
|
+
hostVar: "VELLUM_PLATFORM_URL",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("hostForwarded keeps container name when hostVar differs", () => {
|
|
83
|
+
const spec: DockerStatefulSetSpec = {
|
|
84
|
+
startOrder: ["gateway"],
|
|
85
|
+
readiness: { endpoint: "/readyz", timeoutMs: 1, intervalMs: 1 },
|
|
86
|
+
volumeClaimTemplates: [],
|
|
87
|
+
containers: [
|
|
88
|
+
{
|
|
89
|
+
name: "gateway-sidecar",
|
|
90
|
+
internalName: "gateway",
|
|
91
|
+
network: "container",
|
|
92
|
+
env: [{ kind: "host", name: "CONTAINER_NAME", hostVar: "HOST_NAME" }],
|
|
93
|
+
volumeMounts: [],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const { hostForwarded } = getBuilderManagedEnvKeys("gateway", spec);
|
|
99
|
+
expect(hostForwarded).toEqual([
|
|
100
|
+
{ name: "CONTAINER_NAME", hostVar: "HOST_NAME" },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("throws on unknown service name", () => {
|
|
105
|
+
expect(() => getBuilderManagedEnvKeys("bogus" as ServiceName)).toThrow(
|
|
106
|
+
'docker-statefulset: unknown service "bogus"',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("buildServiceRunArgs extra env routing", () => {
|
|
112
|
+
const opts: BuildServiceRunArgsOpts = {
|
|
113
|
+
gatewayPort: 18080,
|
|
114
|
+
imageTags: {
|
|
115
|
+
assistant: "assistant:test",
|
|
116
|
+
gateway: "gateway:test",
|
|
117
|
+
"credential-executor": "ces:test",
|
|
118
|
+
},
|
|
119
|
+
instanceName: "test-instance",
|
|
120
|
+
res: {
|
|
121
|
+
assistantContainer: "test-assistant",
|
|
122
|
+
cesContainer: "test-ces",
|
|
123
|
+
gatewayContainer: "test-gateway",
|
|
124
|
+
network: "test-net",
|
|
125
|
+
},
|
|
126
|
+
extraGatewayEnv: { VELLUM_DISABLE_PLATFORM: "1" },
|
|
127
|
+
extraAssistantEnv: { FOO: "bar" },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const runArgs = buildServiceRunArgs(opts);
|
|
131
|
+
|
|
132
|
+
test("extraGatewayEnv lands only in gateway args", () => {
|
|
133
|
+
const gatewayArgs = runArgs.gateway();
|
|
134
|
+
expect(gatewayArgs).toContain("VELLUM_DISABLE_PLATFORM=1");
|
|
135
|
+
expect(gatewayArgs).not.toContain("FOO=bar");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("extraAssistantEnv lands only in assistant args", () => {
|
|
139
|
+
const assistantArgs = runArgs.assistant();
|
|
140
|
+
expect(assistantArgs).toContain("FOO=bar");
|
|
141
|
+
expect(assistantArgs).not.toContain("VELLUM_DISABLE_PLATFORM=1");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("credential-executor args get neither extra env map", () => {
|
|
145
|
+
const cesArgs = runArgs["credential-executor"]();
|
|
146
|
+
expect(cesArgs).not.toContain("VELLUM_DISABLE_PLATFORM=1");
|
|
147
|
+
expect(cesArgs).not.toContain("FOO=bar");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { resetHostDeviceIdCache } from "../lib/device-id.js";
|
|
4
|
+
import type { DockerStatefulSetSpec } from "../lib/statefulset.js";
|
|
5
|
+
import { buildReplayEnv, buildReplayState } from "../lib/upgrade-lifecycle.js";
|
|
6
|
+
import { snapshotEnv } from "./helpers/env.js";
|
|
7
|
+
|
|
8
|
+
const restoreEnv = snapshotEnv([
|
|
9
|
+
"VELLUM_PLATFORM_URL",
|
|
10
|
+
"ANTHROPIC_API_KEY",
|
|
11
|
+
"VELLUM_DEVICE_ID",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
restoreEnv();
|
|
16
|
+
resetHostDeviceIdCache();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("buildReplayEnv", () => {
|
|
20
|
+
test("gateway: drops secrets, statics, and PATH; keeps flag overrides", () => {
|
|
21
|
+
const captured = {
|
|
22
|
+
GUARDIAN_BOOTSTRAP_SECRET: "s1",
|
|
23
|
+
CES_SERVICE_TOKEN: "s2",
|
|
24
|
+
ACTOR_TOKEN_SIGNING_KEY: "s3",
|
|
25
|
+
PATH: "/usr/bin",
|
|
26
|
+
GATEWAY_PORT: "18080",
|
|
27
|
+
VELLUM_DISABLE_PLATFORM: "1",
|
|
28
|
+
VELLUM_DEVICE_ID: "abc",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
expect(buildReplayEnv(captured, "gateway")).toEqual({
|
|
32
|
+
VELLUM_DISABLE_PLATFORM: "1",
|
|
33
|
+
VELLUM_DEVICE_ID: "abc",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("gateway: captured VELLUM_PLATFORM_URL dropped when set on host", () => {
|
|
38
|
+
process.env.VELLUM_PLATFORM_URL = "https://host.example.com";
|
|
39
|
+
const replay = buildReplayEnv(
|
|
40
|
+
{ VELLUM_PLATFORM_URL: "https://stale.example.com" },
|
|
41
|
+
"gateway",
|
|
42
|
+
);
|
|
43
|
+
expect(replay).toEqual({});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("gateway: captured VELLUM_PLATFORM_URL kept when unset on host", () => {
|
|
47
|
+
delete process.env.VELLUM_PLATFORM_URL;
|
|
48
|
+
const replay = buildReplayEnv(
|
|
49
|
+
{ VELLUM_PLATFORM_URL: "https://stale.example.com" },
|
|
50
|
+
"gateway",
|
|
51
|
+
);
|
|
52
|
+
expect(replay).toEqual({
|
|
53
|
+
VELLUM_PLATFORM_URL: "https://stale.example.com",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("assistant: drops builder-computed extras, secrets, and PATH; keeps custom flags", () => {
|
|
58
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
59
|
+
const captured = {
|
|
60
|
+
VELLUM_ASSISTANT_NAME: "my-assistant",
|
|
61
|
+
GATEWAY_INTERNAL_URL: "http://localhost:8080",
|
|
62
|
+
GUARDIAN_BOOTSTRAP_SECRET: "s1",
|
|
63
|
+
CES_SERVICE_TOKEN: "s2",
|
|
64
|
+
ACTOR_TOKEN_SIGNING_KEY: "s3",
|
|
65
|
+
PATH: "/usr/bin",
|
|
66
|
+
MY_CUSTOM_FLAG: "yes",
|
|
67
|
+
ANTHROPIC_API_KEY: "sk-captured",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
expect(buildReplayEnv(captured, "assistant")).toEqual({
|
|
71
|
+
MY_CUSTOM_FLAG: "yes",
|
|
72
|
+
ANTHROPIC_API_KEY: "sk-captured",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("assistant: captured ANTHROPIC_API_KEY dropped when set on host", () => {
|
|
77
|
+
process.env.ANTHROPIC_API_KEY = "sk-host";
|
|
78
|
+
const replay = buildReplayEnv(
|
|
79
|
+
{ ANTHROPIC_API_KEY: "sk-captured", MY_CUSTOM_FLAG: "yes" },
|
|
80
|
+
"assistant",
|
|
81
|
+
);
|
|
82
|
+
expect(replay).toEqual({ MY_CUSTOM_FLAG: "yes" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a secret added to the spec is auto-excluded with no code change", () => {
|
|
86
|
+
const spec: DockerStatefulSetSpec = {
|
|
87
|
+
startOrder: ["gateway"],
|
|
88
|
+
readiness: { endpoint: "/readyz", timeoutMs: 1, intervalMs: 1 },
|
|
89
|
+
volumeClaimTemplates: [],
|
|
90
|
+
containers: [
|
|
91
|
+
{
|
|
92
|
+
name: "gateway-sidecar",
|
|
93
|
+
internalName: "gateway",
|
|
94
|
+
network: "container",
|
|
95
|
+
env: [
|
|
96
|
+
{ kind: "secret", name: "FUTURE_SECRET", secret: "signingKey" },
|
|
97
|
+
],
|
|
98
|
+
volumeMounts: [],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const replay = buildReplayEnv(
|
|
104
|
+
{ FUTURE_SECRET: "leaky", VELLUM_DEVICE_ID: "abc" },
|
|
105
|
+
"gateway",
|
|
106
|
+
spec,
|
|
107
|
+
);
|
|
108
|
+
expect(replay).toEqual({ VELLUM_DEVICE_ID: "abc" });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("buildReplayState", () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
// VELLUM_DEVICE_ID env precedence keeps getOrCreateHostDeviceId off the
|
|
115
|
+
// filesystem in tests.
|
|
116
|
+
process.env.VELLUM_DEVICE_ID = "host-device-id";
|
|
117
|
+
resetHostDeviceIdCache();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("backfills VELLUM_DEVICE_ID on gateway replay env when absent", () => {
|
|
121
|
+
const state = buildReplayState({}, { VELLUM_DISABLE_PLATFORM: "1" });
|
|
122
|
+
expect(state.extraGatewayEnv).toEqual({
|
|
123
|
+
VELLUM_DISABLE_PLATFORM: "1",
|
|
124
|
+
VELLUM_DEVICE_ID: "host-device-id",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("captured VELLUM_DEVICE_ID wins over host-derived id", () => {
|
|
129
|
+
const state = buildReplayState({}, { VELLUM_DEVICE_ID: "existing" });
|
|
130
|
+
expect(state.extraGatewayEnv.VELLUM_DEVICE_ID).toBe("existing");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("backfills VELLUM_DEVICE_ID on assistant replay env when absent", () => {
|
|
134
|
+
const state = buildReplayState({}, {});
|
|
135
|
+
expect(state.extraAssistantEnv.VELLUM_DEVICE_ID).toBe("host-device-id");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("assistant backfill inherits captured gateway VELLUM_DEVICE_ID", () => {
|
|
139
|
+
const state = buildReplayState({}, { VELLUM_DEVICE_ID: "gw-captured" });
|
|
140
|
+
expect(state.extraGatewayEnv.VELLUM_DEVICE_ID).toBe("gw-captured");
|
|
141
|
+
expect(state.extraAssistantEnv.VELLUM_DEVICE_ID).toBe("gw-captured");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("captured assistant VELLUM_DEVICE_ID wins over host-derived id", () => {
|
|
145
|
+
const state = buildReplayState({ VELLUM_DEVICE_ID: "existing" }, {});
|
|
146
|
+
expect(state.extraAssistantEnv.VELLUM_DEVICE_ID).toBe("existing");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("plucks secrets from the captured envs", () => {
|
|
150
|
+
const state = buildReplayState(
|
|
151
|
+
{ CES_SERVICE_TOKEN: "ces-token", ACTOR_TOKEN_SIGNING_KEY: "sign-key" },
|
|
152
|
+
{ GUARDIAN_BOOTSTRAP_SECRET: "bootstrap" },
|
|
153
|
+
);
|
|
154
|
+
expect(state.bootstrapSecret).toBe("bootstrap");
|
|
155
|
+
expect(state.cesServiceToken).toBe("ces-token");
|
|
156
|
+
expect(state.signingKey).toBe("sign-key");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("generates fresh secrets when missing from captured env", () => {
|
|
160
|
+
const state = buildReplayState({}, {});
|
|
161
|
+
expect(state.bootstrapSecret).toBeUndefined();
|
|
162
|
+
expect(state.cesServiceToken).toMatch(/^[0-9a-f]{64}$/);
|
|
163
|
+
expect(state.signingKey).toMatch(/^[0-9a-f]{64}$/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -58,10 +58,27 @@ mock.module("../lib/docker.js", () => ({
|
|
|
58
58
|
const seedGuardianTokenFromSiblingEnvMock = mock<
|
|
59
59
|
typeof guardianToken.seedGuardianTokenFromSiblingEnv
|
|
60
60
|
>(() => false);
|
|
61
|
+
// Default: a token exists, so the re-provision recovery path is skipped. Tests
|
|
62
|
+
// that exercise recovery override loadGuardianToken to return null.
|
|
63
|
+
const loadGuardianTokenMock = mock<typeof guardianToken.loadGuardianToken>(
|
|
64
|
+
() => ({ accessToken: "existing" }) as ReturnType<
|
|
65
|
+
typeof guardianToken.loadGuardianToken
|
|
66
|
+
>,
|
|
67
|
+
);
|
|
68
|
+
const resetGuardianBootstrapMock = mock<
|
|
69
|
+
typeof guardianToken.resetGuardianBootstrap
|
|
70
|
+
>(async () => {});
|
|
71
|
+
const leaseGuardianTokenMock = mock<typeof guardianToken.leaseGuardianToken>(
|
|
72
|
+
async () =>
|
|
73
|
+
({}) as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>,
|
|
74
|
+
);
|
|
61
75
|
|
|
62
76
|
mock.module("../lib/guardian-token.js", () => ({
|
|
63
77
|
...realGuardianToken,
|
|
64
78
|
seedGuardianTokenFromSiblingEnv: seedGuardianTokenFromSiblingEnvMock,
|
|
79
|
+
loadGuardianToken: loadGuardianTokenMock,
|
|
80
|
+
resetGuardianBootstrap: resetGuardianBootstrapMock,
|
|
81
|
+
leaseGuardianToken: leaseGuardianTokenMock,
|
|
65
82
|
}));
|
|
66
83
|
|
|
67
84
|
const resolveProcessStateMock = mock<typeof processLib.resolveProcessState>(
|
|
@@ -169,6 +186,16 @@ beforeEach(() => {
|
|
|
169
186
|
startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
|
|
170
187
|
seedGuardianTokenFromSiblingEnvMock.mockReset();
|
|
171
188
|
seedGuardianTokenFromSiblingEnvMock.mockReturnValue(false);
|
|
189
|
+
loadGuardianTokenMock.mockReset();
|
|
190
|
+
loadGuardianTokenMock.mockReturnValue({ accessToken: "existing" } as ReturnType<
|
|
191
|
+
typeof guardianToken.loadGuardianToken
|
|
192
|
+
>);
|
|
193
|
+
resetGuardianBootstrapMock.mockReset();
|
|
194
|
+
resetGuardianBootstrapMock.mockResolvedValue(undefined);
|
|
195
|
+
leaseGuardianTokenMock.mockReset();
|
|
196
|
+
leaseGuardianTokenMock.mockResolvedValue(
|
|
197
|
+
{} as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>,
|
|
198
|
+
);
|
|
172
199
|
maybeStartNgrokTunnelMock.mockReset();
|
|
173
200
|
maybeStartNgrokTunnelMock.mockResolvedValue(null);
|
|
174
201
|
});
|
|
@@ -212,4 +239,45 @@ describe("vellum wake", () => {
|
|
|
212
239
|
},
|
|
213
240
|
);
|
|
214
241
|
});
|
|
242
|
+
|
|
243
|
+
test("re-provisions the guardian token when missing and --repair-guardian is passed", async () => {
|
|
244
|
+
process.argv = ["bun", "vellum", "wake", "--repair-guardian", "local-assistant"];
|
|
245
|
+
loadGuardianTokenMock.mockReturnValue(null);
|
|
246
|
+
|
|
247
|
+
await wake();
|
|
248
|
+
|
|
249
|
+
// Resets the gateway's spent bootstrap state, then re-leases against the
|
|
250
|
+
// loopback gateway with the lockfile's bootstrap secret.
|
|
251
|
+
expect(resetGuardianBootstrapMock).toHaveBeenCalledWith(
|
|
252
|
+
"http://127.0.0.1:7830",
|
|
253
|
+
"generated-bootstrap-secret",
|
|
254
|
+
);
|
|
255
|
+
expect(leaseGuardianTokenMock).toHaveBeenCalledWith(
|
|
256
|
+
"http://127.0.0.1:7830",
|
|
257
|
+
"local-assistant",
|
|
258
|
+
"generated-bootstrap-secret",
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("does NOT re-provision without --repair-guardian, even when the token is missing", async () => {
|
|
263
|
+
// The automatic connect-repair path spawns `wake <id>` with no flags. A
|
|
264
|
+
// re-lease here would revoke other device-bound tokens (other tabs / local
|
|
265
|
+
// clients), so it must never run from auto-repair.
|
|
266
|
+
process.argv = ["bun", "vellum", "wake", "local-assistant"];
|
|
267
|
+
loadGuardianTokenMock.mockReturnValue(null);
|
|
268
|
+
|
|
269
|
+
await wake();
|
|
270
|
+
|
|
271
|
+
expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
|
|
272
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("skips re-provision when a guardian token already exists", async () => {
|
|
276
|
+
process.argv = ["bun", "vellum", "wake", "--repair-guardian", "local-assistant"];
|
|
277
|
+
// loadGuardianToken returns a token by default — recovery must not run.
|
|
278
|
+
await wake();
|
|
279
|
+
|
|
280
|
+
expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
|
|
281
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
282
|
+
});
|
|
215
283
|
});
|
package/src/commands/backup.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
platformRequestSignedUrl,
|
|
17
17
|
readPlatformToken,
|
|
18
18
|
} from "../lib/platform-client.js";
|
|
19
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
19
20
|
|
|
20
21
|
export async function backup(): Promise<void> {
|
|
21
22
|
const args = process.argv.slice(3);
|
|
@@ -112,7 +113,7 @@ export async function backup(): Promise<void> {
|
|
|
112
113
|
// Call the export endpoint
|
|
113
114
|
let response: Response;
|
|
114
115
|
try {
|
|
115
|
-
response = await
|
|
116
|
+
response = await loopbackSafeFetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
116
117
|
method: "POST",
|
|
117
118
|
headers: {
|
|
118
119
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -138,7 +139,7 @@ export async function backup(): Promise<void> {
|
|
|
138
139
|
}
|
|
139
140
|
if (refreshedToken) {
|
|
140
141
|
accessToken = refreshedToken;
|
|
141
|
-
response = await
|
|
142
|
+
response = await loopbackSafeFetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
142
143
|
method: "POST",
|
|
143
144
|
headers: {
|
|
144
145
|
Authorization: `Bearer ${accessToken}`,
|
package/src/commands/client.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
readPlatformToken,
|
|
57
57
|
} from "../lib/platform-client";
|
|
58
58
|
import { tuiLog } from "../lib/tui-log";
|
|
59
|
+
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
59
60
|
|
|
60
61
|
const SUPPORTED_INTERFACES = ["cli", "web"] as const;
|
|
61
62
|
type SupportedInterface = (typeof SUPPORTED_INTERFACES)[number];
|
|
@@ -85,6 +86,7 @@ interface ParsedArgs {
|
|
|
85
86
|
flagEnvVars: Record<string, string>;
|
|
86
87
|
/** Parsed --flag overrides: kebab-case key -> typed value (for web injection). */
|
|
87
88
|
parsedFlagOverrides: Record<string, boolean | string>;
|
|
89
|
+
disablePlatform: boolean;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
function readAssistantName(entry: AssistantEntry | null): string | undefined {
|
|
@@ -99,6 +101,8 @@ export function parseArgs(): ParsedArgs {
|
|
|
99
101
|
const { envVars: cliFlagVars, remaining: argsWithoutFlags } =
|
|
100
102
|
parseFeatureFlagArgs(process.argv.slice(3));
|
|
101
103
|
const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
|
|
104
|
+
const disablePlatformAmbient = process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
|
|
105
|
+
let disablePlatform = disablePlatformAmbient === "true" || disablePlatformAmbient === "1";
|
|
102
106
|
const args = argsWithoutFlags;
|
|
103
107
|
|
|
104
108
|
// Build parsedFlagOverrides from the extracted env vars:
|
|
@@ -133,6 +137,8 @@ export function parseArgs(): ParsedArgs {
|
|
|
133
137
|
if (arg === "--help" || arg === "-h") {
|
|
134
138
|
printUsage();
|
|
135
139
|
process.exit(0);
|
|
140
|
+
} else if (arg === "--disable-platform") {
|
|
141
|
+
disablePlatform = true;
|
|
136
142
|
} else if (
|
|
137
143
|
(arg === "--url" ||
|
|
138
144
|
arg === "-u" ||
|
|
@@ -252,6 +258,7 @@ export function parseArgs(): ParsedArgs {
|
|
|
252
258
|
interfaceId,
|
|
253
259
|
flagEnvVars,
|
|
254
260
|
parsedFlagOverrides,
|
|
261
|
+
disablePlatform,
|
|
255
262
|
};
|
|
256
263
|
}
|
|
257
264
|
|
|
@@ -272,6 +279,7 @@ ${ANSI.bold}OPTIONS:${ANSI.reset}
|
|
|
272
279
|
-a, --assistant-id <id> Assistant ID
|
|
273
280
|
-i, --interface <id> Interface identifier: cli (default) or web
|
|
274
281
|
--flag <key=value> Feature flag override (repeatable, kebab-case key)
|
|
282
|
+
--disable-platform Suppress all outbound platform API calls
|
|
275
283
|
-h, --help Show this help message
|
|
276
284
|
|
|
277
285
|
${ANSI.bold}DEFAULTS:${ANSI.reset}
|
|
@@ -451,6 +459,7 @@ async function handleLocalEndpoints(
|
|
|
451
459
|
result = replacePlatformAssistants(
|
|
452
460
|
lockfilePaths,
|
|
453
461
|
body.platformAssistants as Array<Record<string, unknown>>,
|
|
462
|
+
body.organizationId as string | undefined,
|
|
454
463
|
);
|
|
455
464
|
} else {
|
|
456
465
|
result = upsertLockfileAssistant(
|
|
@@ -611,7 +620,7 @@ async function handleLocalEndpoints(
|
|
|
611
620
|
|
|
612
621
|
try {
|
|
613
622
|
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
614
|
-
const proxyRes = await
|
|
623
|
+
const proxyRes = await loopbackSafeFetch(targetUrl, {
|
|
615
624
|
method: req.method,
|
|
616
625
|
headers,
|
|
617
626
|
body: hasBody ? req.body : undefined,
|
|
@@ -652,6 +661,7 @@ function getBaseDir(): string {
|
|
|
652
661
|
async function runWebInterface(
|
|
653
662
|
flagEnvVars: Record<string, string>,
|
|
654
663
|
parsedFlagOverrides: Record<string, boolean | string>,
|
|
664
|
+
disablePlatform: boolean,
|
|
655
665
|
): Promise<void> {
|
|
656
666
|
// Propagate flag env vars so child processes (e.g. hatch from the web UI) inherit them.
|
|
657
667
|
Object.assign(process.env, flagEnvVars);
|
|
@@ -660,7 +670,7 @@ async function runWebInterface(
|
|
|
660
670
|
// (HMR, __local endpoints, gateway proxy).
|
|
661
671
|
const webSourceDir = findWebSourceDir();
|
|
662
672
|
if (webSourceDir) {
|
|
663
|
-
return runViteDevServer(webSourceDir, flagEnvVars);
|
|
673
|
+
return runViteDevServer(webSourceDir, flagEnvVars, disablePlatform);
|
|
664
674
|
}
|
|
665
675
|
|
|
666
676
|
const distDir = findWebDistDir();
|
|
@@ -679,7 +689,7 @@ async function runWebInterface(
|
|
|
679
689
|
const webUrl = getWebUrl();
|
|
680
690
|
const safeJson = (v: unknown) =>
|
|
681
691
|
JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
682
|
-
const configJson = safeJson({ webUrl, platformUrl });
|
|
692
|
+
const configJson = safeJson({ webUrl, platformUrl, disablePlatform });
|
|
683
693
|
const hasOverrides = Object.keys(parsedFlagOverrides).length > 0;
|
|
684
694
|
const flagOverridesSnippet = hasOverrides
|
|
685
695
|
? `;window.__VELLUM_FLAG_OVERRIDES__=${safeJson(parsedFlagOverrides)}`
|
|
@@ -751,7 +761,7 @@ async function runWebInterface(
|
|
|
751
761
|
try {
|
|
752
762
|
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
753
763
|
const body = hasBody ? await req.arrayBuffer() : undefined;
|
|
754
|
-
const proxyRes = await
|
|
764
|
+
const proxyRes = await loopbackSafeFetch(target.toString(), {
|
|
755
765
|
method: req.method,
|
|
756
766
|
headers,
|
|
757
767
|
body,
|
|
@@ -814,6 +824,7 @@ async function runWebInterface(
|
|
|
814
824
|
async function runViteDevServer(
|
|
815
825
|
webSourceDir: string,
|
|
816
826
|
flagEnvVars: Record<string, string>,
|
|
827
|
+
disablePlatform: boolean,
|
|
817
828
|
): Promise<void> {
|
|
818
829
|
const platformUrl = getPlatformUrl();
|
|
819
830
|
|
|
@@ -830,6 +841,7 @@ async function runViteDevServer(
|
|
|
830
841
|
...process.env,
|
|
831
842
|
...flagEnvVars,
|
|
832
843
|
...viteFlagVars,
|
|
844
|
+
...(disablePlatform ? { VITE_VELLUM_DISABLE_PLATFORM: "true" } : {}),
|
|
833
845
|
VITE_PLATFORM_MODE: "false",
|
|
834
846
|
API_PROXY_TARGET: platformUrl,
|
|
835
847
|
VELLUM_WEB_URL: getWebUrl(),
|
|
@@ -909,10 +921,15 @@ export async function client(): Promise<void> {
|
|
|
909
921
|
interfaceId,
|
|
910
922
|
flagEnvVars,
|
|
911
923
|
parsedFlagOverrides,
|
|
924
|
+
disablePlatform,
|
|
912
925
|
} = parseArgs();
|
|
913
926
|
|
|
927
|
+
if (disablePlatform) {
|
|
928
|
+
process.env.VELLUM_DISABLE_PLATFORM = "true";
|
|
929
|
+
}
|
|
930
|
+
|
|
914
931
|
if (interfaceId === WEB_INTERFACE_ID) {
|
|
915
|
-
await runWebInterface(flagEnvVars, parsedFlagOverrides);
|
|
932
|
+
await runWebInterface(flagEnvVars, parsedFlagOverrides, disablePlatform);
|
|
916
933
|
return;
|
|
917
934
|
}
|
|
918
935
|
|