vigthoria-cli 1.9.5 → 1.9.9
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/README.md +1 -1
- package/dist/commands/auth.js +87 -8
- package/dist/commands/bridge.js +3 -8
- package/dist/commands/chat.js +31 -19
- package/dist/commands/legion.js +20 -11
- package/dist/commands/security.d.ts +20 -0
- package/dist/commands/security.js +98 -0
- package/dist/index.js +41 -15
- package/dist/utils/api.d.ts +6 -5
- package/dist/utils/api.js +166 -48
- package/dist/utils/config.js +1 -1
- package/dist/utils/tools.js +29 -5
- package/install.ps1 +322 -0
- package/install.sh +314 -0
- package/package.json +12 -3
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +159 -0
- package/scripts/release/publish-cli-release.sh +73 -0
- package/scripts/release/validate-no-go-gates.sh +129 -0
- package/scripts/release/verify-runtime-consistency.mjs +64 -0
package/dist/utils/api.d.ts
CHANGED
|
@@ -200,6 +200,7 @@ export declare class APIClient {
|
|
|
200
200
|
private ws;
|
|
201
201
|
private vigFlowTokens;
|
|
202
202
|
private _httpsAgent;
|
|
203
|
+
private lastChatTransportErrors;
|
|
203
204
|
constructor(config: Config, logger: Logger);
|
|
204
205
|
/**
|
|
205
206
|
* Destroy keep-alive sockets so the Node.js event loop can drain
|
|
@@ -217,12 +218,12 @@ export declare class APIClient {
|
|
|
217
218
|
private getAccessToken;
|
|
218
219
|
/**
|
|
219
220
|
* Validate the current auth token against the Coder API.
|
|
220
|
-
*
|
|
221
|
-
* { valid: false, error } when the token is rejected (401/403),
|
|
222
|
-
* and { valid: true } when the server is unreachable (network error)
|
|
223
|
-
* so that offline/degraded scenarios don't block the user.
|
|
221
|
+
* By default this fails open on network errors to keep offline commands usable.
|
|
224
222
|
*/
|
|
225
|
-
validateToken(
|
|
223
|
+
validateToken(options?: {
|
|
224
|
+
allowNetworkFailOpen?: boolean;
|
|
225
|
+
enforceTokenShape?: boolean;
|
|
226
|
+
}): Promise<{
|
|
226
227
|
valid: boolean;
|
|
227
228
|
error?: string;
|
|
228
229
|
}>;
|
package/dist/utils/api.js
CHANGED
|
@@ -100,6 +100,38 @@ function sanitizeUserFacingErrorText(input) {
|
|
|
100
100
|
const withoutTags = raw.replace(/<[^>]+>/g, ' ');
|
|
101
101
|
return withoutTags.replace(/\s+/g, ' ').trim();
|
|
102
102
|
}
|
|
103
|
+
const TRUSTED_TOKEN_HOST_PATTERN = /(^|\.)vigthoria\.io$/i;
|
|
104
|
+
function isLoopbackHost(hostname) {
|
|
105
|
+
const host = String(hostname || '').toLowerCase();
|
|
106
|
+
return host === 'localhost' || host === '127.0.0.1';
|
|
107
|
+
}
|
|
108
|
+
function isTrustedTokenDestination(rawUrl) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = new URL(rawUrl);
|
|
111
|
+
const host = parsed.hostname.toLowerCase();
|
|
112
|
+
return TRUSTED_TOKEN_HOST_PATTERN.test(host) || isLoopbackHost(host);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function resolveAxiosRequestUrl(req) {
|
|
119
|
+
const direct = String(req?.url || '').trim();
|
|
120
|
+
const base = String(req?.baseURL || '').trim();
|
|
121
|
+
if (!direct && !base)
|
|
122
|
+
return '';
|
|
123
|
+
if (/^https?:\/\//i.test(direct))
|
|
124
|
+
return direct;
|
|
125
|
+
if (base) {
|
|
126
|
+
try {
|
|
127
|
+
return new URL(direct || '', base).toString();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return direct;
|
|
134
|
+
}
|
|
103
135
|
function isServerRuntime() {
|
|
104
136
|
if (process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1') {
|
|
105
137
|
return true;
|
|
@@ -146,9 +178,12 @@ function propagateError(err) {
|
|
|
146
178
|
};
|
|
147
179
|
}
|
|
148
180
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
149
|
-
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS
|
|
181
|
+
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
182
|
+
if (!rawValue) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
150
185
|
const parsed = Number.parseInt(rawValue, 10);
|
|
151
|
-
return Number.isFinite(parsed) && parsed
|
|
186
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
152
187
|
})();
|
|
153
188
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
154
189
|
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
|
|
@@ -156,9 +191,12 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
|
156
191
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
157
192
|
})();
|
|
158
193
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
159
|
-
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS
|
|
194
|
+
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
|
|
195
|
+
if (!rawValue) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
160
198
|
const parsed = Number.parseInt(rawValue, 10);
|
|
161
|
-
return Number.isFinite(parsed) && parsed
|
|
199
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
162
200
|
})();
|
|
163
201
|
class APIClient {
|
|
164
202
|
client;
|
|
@@ -169,6 +207,7 @@ class APIClient {
|
|
|
169
207
|
ws = null;
|
|
170
208
|
vigFlowTokens = new Map();
|
|
171
209
|
_httpsAgent = null;
|
|
210
|
+
lastChatTransportErrors = [];
|
|
172
211
|
constructor(config, logger) {
|
|
173
212
|
this.config = config;
|
|
174
213
|
this.logger = logger;
|
|
@@ -212,24 +251,27 @@ class APIClient {
|
|
|
212
251
|
}) : null;
|
|
213
252
|
// Add auth interceptor
|
|
214
253
|
this.client.interceptors.request.use((req) => {
|
|
215
|
-
const token = this.
|
|
216
|
-
|
|
254
|
+
const token = this.getAccessToken();
|
|
255
|
+
const destination = resolveAxiosRequestUrl(req);
|
|
256
|
+
if (token && isTrustedTokenDestination(destination)) {
|
|
217
257
|
req.headers.Authorization = `Bearer ${token}`;
|
|
218
258
|
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
219
259
|
}
|
|
220
260
|
return req;
|
|
221
261
|
});
|
|
222
262
|
this.modelRouterClient.interceptors.request.use((req) => {
|
|
223
|
-
const token = this.
|
|
224
|
-
|
|
263
|
+
const token = this.getAccessToken();
|
|
264
|
+
const destination = resolveAxiosRequestUrl(req);
|
|
265
|
+
if (token && isTrustedTokenDestination(destination)) {
|
|
225
266
|
req.headers.Authorization = `Bearer ${token}`;
|
|
226
267
|
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
227
268
|
}
|
|
228
269
|
return req;
|
|
229
270
|
});
|
|
230
271
|
this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
|
|
231
|
-
const token = this.
|
|
232
|
-
|
|
272
|
+
const token = this.getAccessToken();
|
|
273
|
+
const destination = resolveAxiosRequestUrl(req);
|
|
274
|
+
if (token && isTrustedTokenDestination(destination)) {
|
|
233
275
|
req.headers.Authorization = `Bearer ${token}`;
|
|
234
276
|
}
|
|
235
277
|
return req;
|
|
@@ -420,28 +462,39 @@ class APIClient {
|
|
|
420
462
|
}
|
|
421
463
|
/**
|
|
422
464
|
* Validate the current auth token against the Coder API.
|
|
423
|
-
*
|
|
424
|
-
* { valid: false, error } when the token is rejected (401/403),
|
|
425
|
-
* and { valid: true } when the server is unreachable (network error)
|
|
426
|
-
* so that offline/degraded scenarios don't block the user.
|
|
465
|
+
* By default this fails open on network errors to keep offline commands usable.
|
|
427
466
|
*/
|
|
428
|
-
async validateToken() {
|
|
467
|
+
async validateToken(options = {}) {
|
|
468
|
+
const allowNetworkFailOpen = options.allowNetworkFailOpen !== false;
|
|
469
|
+
const enforceTokenShape = options.enforceTokenShape !== false;
|
|
429
470
|
const token = this.getAccessToken();
|
|
430
471
|
if (!token) {
|
|
431
472
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
432
473
|
}
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
474
|
+
// Fast-fail obviously malformed tokens so invalid-token checks don't get
|
|
475
|
+
// masked by unrelated transport outages.
|
|
476
|
+
if (enforceTokenShape) {
|
|
477
|
+
const looksLikeJwt = token.split('.').length === 3;
|
|
478
|
+
if (!looksLikeJwt || token.length < 40) {
|
|
479
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
|
|
483
|
+
const headers = {
|
|
484
|
+
Authorization: `Bearer ${token}`,
|
|
485
|
+
Cookie: `vigthoria-auth-token=${token}`,
|
|
486
|
+
};
|
|
487
|
+
const canonicalBaseUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
488
|
+
// Probe protected canonical endpoints in parallel so stale local endpoint overrides
|
|
489
|
+
// cannot mask an invalid gateway token during preflight.
|
|
436
490
|
const results = await Promise.allSettled([
|
|
437
|
-
|
|
438
|
-
|
|
491
|
+
axios_1.default.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
|
|
492
|
+
axios_1.default.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
|
|
439
493
|
]);
|
|
440
494
|
for (const r of results) {
|
|
441
495
|
if (r.status === 'fulfilled')
|
|
442
496
|
return { valid: true };
|
|
443
497
|
}
|
|
444
|
-
// Both failed — check why
|
|
445
498
|
for (const r of results) {
|
|
446
499
|
if (r.status === 'rejected') {
|
|
447
500
|
const err = r.reason;
|
|
@@ -453,7 +506,10 @@ class APIClient {
|
|
|
453
506
|
}
|
|
454
507
|
}
|
|
455
508
|
}
|
|
456
|
-
|
|
509
|
+
if (explicitEnvToken || !allowNetworkFailOpen) {
|
|
510
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
511
|
+
}
|
|
512
|
+
// Both unreachable — don't assume the stored token is bad when running offline.
|
|
457
513
|
return { valid: true };
|
|
458
514
|
}
|
|
459
515
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
@@ -464,6 +520,7 @@ class APIClient {
|
|
|
464
520
|
process.env.V3_AGENT_URL,
|
|
465
521
|
...(allowLocalV3Agent ? ['http://127.0.0.1:8030'] : []),
|
|
466
522
|
configuredApiUrl,
|
|
523
|
+
'https://coder.vigthoria.io',
|
|
467
524
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
468
525
|
return [...new Set(urls)];
|
|
469
526
|
}
|
|
@@ -486,6 +543,7 @@ class APIClient {
|
|
|
486
543
|
process.env.OPERATOR_URL,
|
|
487
544
|
'http://127.0.0.1:4009',
|
|
488
545
|
configuredModelsApiUrl,
|
|
546
|
+
'https://api.vigthoria.io',
|
|
489
547
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
490
548
|
return [...new Set(urls)];
|
|
491
549
|
}
|
|
@@ -908,6 +966,8 @@ class APIClient {
|
|
|
908
966
|
if (authToken) {
|
|
909
967
|
headers.Authorization = `Bearer ${authToken}`;
|
|
910
968
|
headers.Cookie = `vigthoria-auth-token=${authToken}`;
|
|
969
|
+
headers['X-Vigthoria-Token'] = authToken;
|
|
970
|
+
headers['X-Auth-Token'] = authToken;
|
|
911
971
|
}
|
|
912
972
|
return headers;
|
|
913
973
|
}
|
|
@@ -2769,7 +2829,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2769
2829
|
}
|
|
2770
2830
|
async runV3AgentWorkflow(message, context = {}) {
|
|
2771
2831
|
const executionContext = await this.bindExecutionContext(context);
|
|
2772
|
-
const
|
|
2832
|
+
const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
|
|
2833
|
+
const baseTimeoutMs = Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 ? requestedTimeoutMs : 0;
|
|
2773
2834
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
|
|
2774
2835
|
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
2775
2836
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
@@ -2777,7 +2838,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2777
2838
|
&& context.localMachineCapable !== false;
|
|
2778
2839
|
const rescueEligibleSaaS = preferLocalV3
|
|
2779
2840
|
&& /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
|
|
2780
|
-
const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
|
|
2841
|
+
const timeoutMs = baseTimeoutMs > 0 && rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
|
|
2781
2842
|
const maxAttempts = preferLocalV3 ? 2 : 1;
|
|
2782
2843
|
let lastErrors = [];
|
|
2783
2844
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -2811,7 +2872,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2811
2872
|
};
|
|
2812
2873
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
2813
2874
|
const controller = new AbortController();
|
|
2814
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2875
|
+
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
2815
2876
|
try {
|
|
2816
2877
|
const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
|
|
2817
2878
|
if (!response.ok) {
|
|
@@ -2843,7 +2904,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2843
2904
|
stream: true,
|
|
2844
2905
|
};
|
|
2845
2906
|
const continueController = new AbortController();
|
|
2846
|
-
const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
|
|
2907
|
+
const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
|
|
2847
2908
|
try {
|
|
2848
2909
|
const continueHeaders = await this.getV3AgentHeaders();
|
|
2849
2910
|
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
@@ -2861,7 +2922,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2861
2922
|
break; // Fall through to normal completion with partial data
|
|
2862
2923
|
}
|
|
2863
2924
|
finally {
|
|
2864
|
-
|
|
2925
|
+
if (continueTimeoutId)
|
|
2926
|
+
clearTimeout(continueTimeoutId);
|
|
2865
2927
|
}
|
|
2866
2928
|
}
|
|
2867
2929
|
// Use the final continuation data for workspace recovery
|
|
@@ -2915,7 +2977,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2915
2977
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
2916
2978
|
}
|
|
2917
2979
|
finally {
|
|
2918
|
-
|
|
2980
|
+
if (timeoutId)
|
|
2981
|
+
clearTimeout(timeoutId);
|
|
2919
2982
|
}
|
|
2920
2983
|
}
|
|
2921
2984
|
lastErrors = errors;
|
|
@@ -2934,8 +2997,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2934
2997
|
&& !process.env.VIGTHORIA_AUTH_TOKEN
|
|
2935
2998
|
&& Boolean(this.config.get('authToken'));
|
|
2936
2999
|
if (onlyUnauthorizedErrors && usingStoredConfigToken) {
|
|
2937
|
-
this.
|
|
2938
|
-
|
|
3000
|
+
const gatewayTokenCheck = await this.validateToken({ allowNetworkFailOpen: true, enforceTokenShape: true });
|
|
3001
|
+
if (!gatewayTokenCheck.valid) {
|
|
3002
|
+
this.config.clearAuth();
|
|
3003
|
+
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
3004
|
+
}
|
|
3005
|
+
throw new Error('V3 agent authentication failed at the V3 service layer while your gateway login token is still valid. Please retry shortly.');
|
|
2939
3006
|
}
|
|
2940
3007
|
if (preferLocalV3
|
|
2941
3008
|
&& !this.hasAgentWorkspaceOutput(executionContext)
|
|
@@ -2982,7 +3049,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2982
3049
|
}
|
|
2983
3050
|
async runOperatorWorkflow(message, context = {}) {
|
|
2984
3051
|
const executionContext = await this.bindExecutionContext(context);
|
|
2985
|
-
const
|
|
3052
|
+
const requestedOperatorTimeoutMs = Number(context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS);
|
|
3053
|
+
const timeoutMs = Number.isFinite(requestedOperatorTimeoutMs) && requestedOperatorTimeoutMs > 0
|
|
3054
|
+
? requestedOperatorTimeoutMs
|
|
3055
|
+
: 0;
|
|
2986
3056
|
const errors = [];
|
|
2987
3057
|
const authToken = this.config.get('authToken');
|
|
2988
3058
|
// Collect a lightweight workspace file listing so the operator can
|
|
@@ -2991,7 +3061,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2991
3061
|
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
2992
3062
|
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
2993
3063
|
const controller = new AbortController();
|
|
2994
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3064
|
+
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
2995
3065
|
try {
|
|
2996
3066
|
const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
|
|
2997
3067
|
method: 'POST',
|
|
@@ -3138,7 +3208,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3138
3208
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3139
3209
|
}
|
|
3140
3210
|
finally {
|
|
3141
|
-
|
|
3211
|
+
if (timeoutId)
|
|
3212
|
+
clearTimeout(timeoutId);
|
|
3142
3213
|
}
|
|
3143
3214
|
}
|
|
3144
3215
|
throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
|
|
@@ -3155,6 +3226,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3155
3226
|
* NO localhost fallbacks - CLI is for external users, not server-side!
|
|
3156
3227
|
*/
|
|
3157
3228
|
async chat(messages, model, useLocal = false) {
|
|
3229
|
+
this.lastChatTransportErrors = [];
|
|
3158
3230
|
const resolvedModel = this.resolveModelId(model);
|
|
3159
3231
|
const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
|
|
3160
3232
|
? [this.getSelfHostedFallbackModelId(resolvedModel, model)]
|
|
@@ -3173,12 +3245,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3173
3245
|
}
|
|
3174
3246
|
}
|
|
3175
3247
|
// No more localhost fallbacks - CLI is for external users!
|
|
3176
|
-
|
|
3248
|
+
const detail = this.lastChatTransportErrors.length > 0
|
|
3249
|
+
? ` Tried routes: ${this.lastChatTransportErrors.slice(0, 4).join(' | ')}`
|
|
3250
|
+
: '';
|
|
3251
|
+
throw new CLIError(`AI service unavailable. Please check your internet connection or try again later.${detail}`, 'model_backend');
|
|
3177
3252
|
}
|
|
3178
3253
|
shouldSkipCloudRoutes(resolvedModel) {
|
|
3179
3254
|
return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
|
|
3180
3255
|
}
|
|
3181
3256
|
async tryChatWithModel(messages, resolvedModel, requestedModel) {
|
|
3257
|
+
const routeFailures = [];
|
|
3182
3258
|
const preferSelfHostedFirst = this.isSelfHostedPreferredModel(resolvedModel, requestedModel);
|
|
3183
3259
|
if (preferSelfHostedFirst) {
|
|
3184
3260
|
const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
|
|
@@ -3190,7 +3266,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3190
3266
|
if (!this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
3191
3267
|
try {
|
|
3192
3268
|
this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
|
|
3193
|
-
const token = this.
|
|
3269
|
+
const token = this.getAccessToken();
|
|
3194
3270
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3195
3271
|
model: resolvedModel,
|
|
3196
3272
|
messages,
|
|
@@ -3219,6 +3295,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3219
3295
|
catch (error) {
|
|
3220
3296
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3221
3297
|
this.logger.debug(`Direct Vigthoria Models API failed for ${resolvedModel}: ${errMsg}`);
|
|
3298
|
+
routeFailures.push(`models:${String(errMsg).slice(0, 120)}`);
|
|
3222
3299
|
}
|
|
3223
3300
|
}
|
|
3224
3301
|
else {
|
|
@@ -3253,6 +3330,37 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3253
3330
|
catch (error) {
|
|
3254
3331
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3255
3332
|
this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
|
|
3333
|
+
routeFailures.push(`coder:${String(errMsg).slice(0, 120)}`);
|
|
3334
|
+
}
|
|
3335
|
+
try {
|
|
3336
|
+
this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
|
|
3337
|
+
const token = this.getAccessToken();
|
|
3338
|
+
const response = await axios_1.default.post('https://coder.vigthoria.io/api/ai/chat', {
|
|
3339
|
+
messages,
|
|
3340
|
+
model: resolvedModel,
|
|
3341
|
+
maxTokens: this.config.get('preferences').maxTokens,
|
|
3342
|
+
temperature: 0.7,
|
|
3343
|
+
}, {
|
|
3344
|
+
timeout: 180000,
|
|
3345
|
+
httpsAgent: this._httpsAgent ?? undefined,
|
|
3346
|
+
headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
|
|
3347
|
+
});
|
|
3348
|
+
if (response.data?.success !== false) {
|
|
3349
|
+
const content = response.data.response || response.data.message || response.data.content;
|
|
3350
|
+
if (typeof content === 'string' && content.trim()) {
|
|
3351
|
+
return {
|
|
3352
|
+
id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
|
|
3353
|
+
message: content,
|
|
3354
|
+
model: response.data.model || resolvedModel || requestedModel,
|
|
3355
|
+
usage: response.data.usage,
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
catch (error) {
|
|
3361
|
+
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3362
|
+
this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
|
|
3363
|
+
routeFailures.push(`coder-canonical:${String(errMsg).slice(0, 120)}`);
|
|
3256
3364
|
}
|
|
3257
3365
|
}
|
|
3258
3366
|
if (!preferSelfHostedFirst) {
|
|
@@ -3261,6 +3369,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3261
3369
|
return selfHostedResponse;
|
|
3262
3370
|
}
|
|
3263
3371
|
}
|
|
3372
|
+
if (routeFailures.length > 0) {
|
|
3373
|
+
this.lastChatTransportErrors = routeFailures;
|
|
3374
|
+
}
|
|
3264
3375
|
return null;
|
|
3265
3376
|
}
|
|
3266
3377
|
async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
|
|
@@ -3311,7 +3422,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3311
3422
|
'vigthoria-cloud-ultra',
|
|
3312
3423
|
]);
|
|
3313
3424
|
if (cloudModels.has(resolvedModel)) {
|
|
3314
|
-
return 'vigthoria-v3-code-
|
|
3425
|
+
return 'vigthoria-v3-code-35b';
|
|
3315
3426
|
}
|
|
3316
3427
|
return null;
|
|
3317
3428
|
}
|
|
@@ -3326,6 +3437,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3326
3437
|
return this.config.hasCloudAccess();
|
|
3327
3438
|
}
|
|
3328
3439
|
resolvePermittedModelId(shortName) {
|
|
3440
|
+
const normalizedRequested = String(shortName || '').trim().toLowerCase();
|
|
3441
|
+
const blockedModels = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
|
|
3442
|
+
if (blockedModels.has(normalizedRequested)) {
|
|
3443
|
+
this.logger.debug(`Blocked governed model ${shortName}; using fallback vigthoria-v3-code-35b`);
|
|
3444
|
+
return 'vigthoria-v3-code-35b';
|
|
3445
|
+
}
|
|
3329
3446
|
const resolvedModel = this.resolveModelId(shortName);
|
|
3330
3447
|
if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
|
|
3331
3448
|
const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
|
|
@@ -3350,8 +3467,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3350
3467
|
isSelfHostedPreferredModel(resolvedModel, requestedModel) {
|
|
3351
3468
|
const normalizedRequested = String(requestedModel || '').toLowerCase();
|
|
3352
3469
|
const selfHostedModels = new Set([
|
|
3353
|
-
'vigthoria-v3-code-
|
|
3354
|
-
'vigthoria-v3-code-
|
|
3470
|
+
'vigthoria-v3-code-35b',
|
|
3471
|
+
'vigthoria-v3-code-35b:latest',
|
|
3355
3472
|
'qwen3-coder:latest',
|
|
3356
3473
|
'vigthoria-v2-code-8b',
|
|
3357
3474
|
]);
|
|
@@ -3363,9 +3480,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3363
3480
|
}
|
|
3364
3481
|
getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
|
|
3365
3482
|
if (this.isSelfHostedPreferredModel(resolvedModel, requestedModel)) {
|
|
3366
|
-
return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-
|
|
3483
|
+
return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-35b' : resolvedModel;
|
|
3367
3484
|
}
|
|
3368
|
-
return 'vigthoria-v3-code-
|
|
3485
|
+
return 'vigthoria-v3-code-35b';
|
|
3369
3486
|
}
|
|
3370
3487
|
// Streaming chat
|
|
3371
3488
|
async *chatStream(messages, model) {
|
|
@@ -3448,7 +3565,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3448
3565
|
// (/v1/chat/completions on api.vigthoria.io) which is the only
|
|
3449
3566
|
// backend that reliably accepts our auth token.
|
|
3450
3567
|
async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
3451
|
-
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-
|
|
3568
|
+
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-35b';
|
|
3452
3569
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3453
3570
|
model: resolvedModel,
|
|
3454
3571
|
messages: [
|
|
@@ -4301,15 +4418,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4301
4418
|
'fast': 'vigthoria-fast-1.7b',
|
|
4302
4419
|
'mini': 'vigthoria-mini-0.6b',
|
|
4303
4420
|
'balanced': 'vigthoria-balanced-4b',
|
|
4421
|
+
'balanced-4b': 'vigthoria-balanced-4b',
|
|
4304
4422
|
'creative': 'vigthoria-creative-9b-v4',
|
|
4305
4423
|
// Code Models - 30B is the default powerhouse
|
|
4306
|
-
'code': 'vigthoria-v3-code-
|
|
4307
|
-
'code-30b': 'vigthoria-v3-code-
|
|
4424
|
+
'code': 'vigthoria-v3-code-35b', // Internal: self-hosted 35B on Blackwell
|
|
4425
|
+
'code-30b': 'vigthoria-v3-code-35b',
|
|
4308
4426
|
'code-8b': 'vigthoria-v2-code-8b',
|
|
4309
|
-
'pro': 'vigthoria-v3-code-
|
|
4310
|
-
'agent': 'vigthoria-v3-code-
|
|
4311
|
-
'vigthoria-code': 'vigthoria-v3-code-
|
|
4312
|
-
'vigthoria-agent': 'vigthoria-v3-code-
|
|
4427
|
+
'pro': 'vigthoria-v3-code-35b',
|
|
4428
|
+
'agent': 'vigthoria-v3-code-35b',
|
|
4429
|
+
'vigthoria-code': 'vigthoria-v3-code-35b',
|
|
4430
|
+
'vigthoria-agent': 'vigthoria-v3-code-35b',
|
|
4313
4431
|
// ═══════════════════════════════════════════════════════════════
|
|
4314
4432
|
// VIGTHORIA CLOUD - Premium cloud models (internal routing)
|
|
4315
4433
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -4324,7 +4442,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4324
4442
|
}
|
|
4325
4443
|
return shortName;
|
|
4326
4444
|
}
|
|
4327
|
-
return modelMap[shortName] || 'vigthoria-v3-code-
|
|
4445
|
+
return modelMap[shortName] || 'vigthoria-v3-code-35b';
|
|
4328
4446
|
}
|
|
4329
4447
|
async getCoderHealth() {
|
|
4330
4448
|
try {
|
package/dist/utils/config.js
CHANGED
|
@@ -47,7 +47,7 @@ const defaultConfig = {
|
|
|
47
47
|
};
|
|
48
48
|
class Config {
|
|
49
49
|
store;
|
|
50
|
-
static OPERATOR_PLANS = new Set(['enterprise', 'admin', 'master_admin']);
|
|
50
|
+
static OPERATOR_PLANS = new Set(['pro', 'professional', 'ultra', 'enterprise', 'admin', 'master_admin']);
|
|
51
51
|
static CLOUD_PLANS = new Set(['pro', 'professional', 'ultra', 'enterprise', 'master_admin', 'admin']);
|
|
52
52
|
constructor() {
|
|
53
53
|
this.store = new conf_1.default({
|
package/dist/utils/tools.js
CHANGED
|
@@ -61,6 +61,17 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
61
61
|
const logger_js_1 = require("./logger.js");
|
|
62
62
|
const api_js_1 = require("./api.js");
|
|
63
63
|
const STREAM_RESPONSE_MAX_YIELD_CHARS = 32 * 1024;
|
|
64
|
+
const POWERSHELL_SAFE_PATH_PATTERN = /^[A-Za-z0-9_:\\/.\-\s]+$/;
|
|
65
|
+
const POWERSHELL_SAFE_INCLUDE_PATTERN = /^[A-Za-z0-9_*?.\-]+$/;
|
|
66
|
+
const SSH_SAFE_HOST_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
67
|
+
function getSshAllowedHosts() {
|
|
68
|
+
const configured = String(process.env.VIGTHORIA_SSH_ALLOWED_HOSTS || '')
|
|
69
|
+
.split(',')
|
|
70
|
+
.map((v) => v.trim().toLowerCase())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
const defaults = ['vigthoria-server', 'localhost', '127.0.0.1'];
|
|
73
|
+
return new Set([...defaults, ...configured]);
|
|
74
|
+
}
|
|
64
75
|
function isNodeError(error) {
|
|
65
76
|
return error instanceof Error && 'code' in error;
|
|
66
77
|
}
|
|
@@ -1798,9 +1809,14 @@ class AgenticTools {
|
|
|
1798
1809
|
grepWithSelectString(args, searchPath) {
|
|
1799
1810
|
// Normalize path for PowerShell
|
|
1800
1811
|
const psPath = searchPath.replace(/\//g, '\\');
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1812
|
+
if (!POWERSHELL_SAFE_PATH_PATTERN.test(psPath)) {
|
|
1813
|
+
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Unsafe search path for PowerShell backend', 'Use a normal workspace path containing only letters, numbers, separators, dot, dash, and spaces.');
|
|
1814
|
+
}
|
|
1815
|
+
const includeArg = args.include ? String(args.include) : '*';
|
|
1816
|
+
if (!POWERSHELL_SAFE_INCLUDE_PATTERN.test(includeArg)) {
|
|
1817
|
+
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Unsafe include filter for PowerShell backend', 'Use simple wildcard filters like *.ts or *.py.');
|
|
1818
|
+
}
|
|
1819
|
+
const includeFilter = `-Include "${includeArg}"`;
|
|
1804
1820
|
const escapedPattern = args.pattern.replace(/'/g, "''");
|
|
1805
1821
|
const cmd = `powershell -NoProfile -Command "Get-ChildItem -Path '${psPath}' -Recurse -File ${includeFilter} | Select-String -Pattern '${escapedPattern}' | ForEach-Object { $_.Path + ':' + $_.LineNumber + ':' + $_.Line }"`;
|
|
1806
1822
|
try {
|
|
@@ -2453,6 +2469,14 @@ class AgenticTools {
|
|
|
2453
2469
|
const command = args.command;
|
|
2454
2470
|
const host = args.host || 'vigthoria-server';
|
|
2455
2471
|
const timeout = args.timeout ? parseInt(args.timeout) * 1000 : 60000;
|
|
2472
|
+
const normalizedHost = String(host).trim().toLowerCase();
|
|
2473
|
+
if (!SSH_SAFE_HOST_PATTERN.test(normalizedHost)) {
|
|
2474
|
+
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Invalid SSH host format', 'Host can only include letters, numbers, dots, and dashes.');
|
|
2475
|
+
}
|
|
2476
|
+
const allowedHosts = getSshAllowedHosts();
|
|
2477
|
+
if (!allowedHosts.has(normalizedHost)) {
|
|
2478
|
+
return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `SSH host is not allowlisted: ${host}`, 'Add the host to VIGTHORIA_SSH_ALLOWED_HOSTS to permit access.');
|
|
2479
|
+
}
|
|
2456
2480
|
// Security checks for SSH commands
|
|
2457
2481
|
const blockedPatterns = [
|
|
2458
2482
|
/\brm\s+-rf?\s+\//i, // Dangerous rm commands
|
|
@@ -2478,12 +2502,12 @@ class AgenticTools {
|
|
|
2478
2502
|
};
|
|
2479
2503
|
if (platform === 'win32') {
|
|
2480
2504
|
// On Windows, use the ssh command from OpenSSH
|
|
2481
|
-
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=
|
|
2505
|
+
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${host} "${command.replace(/"/g, '\\"')}"`;
|
|
2482
2506
|
execOptions.shell = true;
|
|
2483
2507
|
}
|
|
2484
2508
|
else {
|
|
2485
2509
|
// On Unix-like systems
|
|
2486
|
-
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=
|
|
2510
|
+
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${host} '${command.replace(/'/g, "'\\''")}'`;
|
|
2487
2511
|
execOptions.shell = '/bin/sh';
|
|
2488
2512
|
}
|
|
2489
2513
|
const output = (0, child_process_1.execSync)(sshCommand, execOptions);
|