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.
@@ -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
- * Returns { valid: true } when the server accepts the token,
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(): Promise<{
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 || '1200000';
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 > 0 ? parsed : 1200000;
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 || '300000';
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 > 0 ? parsed : 300000;
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.config.get('authToken');
216
- if (token) {
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.config.get('authToken');
224
- if (token) {
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.config.get('authToken');
232
- if (token) {
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
- * Returns { valid: true } when the server accepts the token,
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
- // Probe both endpoints in parallel. If EITHER succeeds the token is
434
- // valid. Only if both return 401/403 is the token truly invalid.
435
- // If both are unreachable assume the token is fine (offline scenario).
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
- this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
438
- this.client.get('/api/user/profile', { timeout: 5000 }),
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
- // Both unreachable don't assume token is bad
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 baseTimeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
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
- clearTimeout(continueTimeoutId);
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
- clearTimeout(timeoutId);
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.config.clearAuth();
2938
- throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
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 timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
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
- clearTimeout(timeoutId);
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
- throw new CLIError('AI service unavailable. Please check your internet connection or try again later.', 'model_backend');
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.config.get('authToken');
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-30b';
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-30b',
3354
- 'vigthoria-v3-code-30b:latest',
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-30b' : resolvedModel;
3483
+ return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-35b' : resolvedModel;
3367
3484
  }
3368
- return 'vigthoria-v3-code-30b';
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-30b';
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-30b', // Internal: self-hosted 30B on Blackwell
4307
- 'code-30b': 'vigthoria-v3-code-30b',
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-30b',
4310
- 'agent': 'vigthoria-v3-code-30b',
4311
- 'vigthoria-code': 'vigthoria-v3-code-30b',
4312
- 'vigthoria-agent': 'vigthoria-v3-code-30b',
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-30b';
4445
+ return modelMap[shortName] || 'vigthoria-v3-code-35b';
4328
4446
  }
4329
4447
  async getCoderHealth() {
4330
4448
  try {
@@ -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({
@@ -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
- const includeFilter = args.include
1802
- ? `-Include "${args.include}"`
1803
- : `-Include *`;
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=no -o ConnectTimeout=10 ${host} "${command.replace(/"/g, '\\"')}"`;
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=no -o ConnectTimeout=10 ${host} '${command.replace(/'/g, "'\\''")}'`;
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);