vigthoria-cli 1.9.5 → 1.9.8

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/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;
@@ -212,24 +250,27 @@ class APIClient {
212
250
  }) : null;
213
251
  // Add auth interceptor
214
252
  this.client.interceptors.request.use((req) => {
215
- const token = this.config.get('authToken');
216
- if (token) {
253
+ const token = this.getAccessToken();
254
+ const destination = resolveAxiosRequestUrl(req);
255
+ if (token && isTrustedTokenDestination(destination)) {
217
256
  req.headers.Authorization = `Bearer ${token}`;
218
257
  req.headers.Cookie = `vigthoria-auth-token=${token}`;
219
258
  }
220
259
  return req;
221
260
  });
222
261
  this.modelRouterClient.interceptors.request.use((req) => {
223
- const token = this.config.get('authToken');
224
- if (token) {
262
+ const token = this.getAccessToken();
263
+ const destination = resolveAxiosRequestUrl(req);
264
+ if (token && isTrustedTokenDestination(destination)) {
225
265
  req.headers.Authorization = `Bearer ${token}`;
226
266
  req.headers.Cookie = `vigthoria-auth-token=${token}`;
227
267
  }
228
268
  return req;
229
269
  });
230
270
  this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
231
- const token = this.config.get('authToken');
232
- if (token) {
271
+ const token = this.getAccessToken();
272
+ const destination = resolveAxiosRequestUrl(req);
273
+ if (token && isTrustedTokenDestination(destination)) {
233
274
  req.headers.Authorization = `Bearer ${token}`;
234
275
  }
235
276
  return req;
@@ -430,18 +471,22 @@ class APIClient {
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
+ const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
475
+ const headers = {
476
+ Authorization: `Bearer ${token}`,
477
+ Cookie: `vigthoria-auth-token=${token}`,
478
+ };
479
+ const canonicalBaseUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
480
+ // Probe protected canonical endpoints in parallel so stale local endpoint overrides
481
+ // cannot mask an invalid gateway token during preflight.
436
482
  const results = await Promise.allSettled([
437
- this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
438
- this.client.get('/api/user/profile', { timeout: 5000 }),
483
+ axios_1.default.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
484
+ axios_1.default.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
439
485
  ]);
440
486
  for (const r of results) {
441
487
  if (r.status === 'fulfilled')
442
488
  return { valid: true };
443
489
  }
444
- // Both failed — check why
445
490
  for (const r of results) {
446
491
  if (r.status === 'rejected') {
447
492
  const err = r.reason;
@@ -453,7 +498,10 @@ class APIClient {
453
498
  }
454
499
  }
455
500
  }
456
- // Both unreachable — don't assume token is bad
501
+ if (explicitEnvToken) {
502
+ return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
503
+ }
504
+ // Both unreachable — don't assume the stored token is bad when running offline.
457
505
  return { valid: true };
458
506
  }
459
507
  getV3AgentBaseUrls(preferLocal = false) {
@@ -464,6 +512,7 @@ class APIClient {
464
512
  process.env.V3_AGENT_URL,
465
513
  ...(allowLocalV3Agent ? ['http://127.0.0.1:8030'] : []),
466
514
  configuredApiUrl,
515
+ 'https://coder.vigthoria.io',
467
516
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
468
517
  return [...new Set(urls)];
469
518
  }
@@ -486,6 +535,7 @@ class APIClient {
486
535
  process.env.OPERATOR_URL,
487
536
  'http://127.0.0.1:4009',
488
537
  configuredModelsApiUrl,
538
+ 'https://api.vigthoria.io',
489
539
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
490
540
  return [...new Set(urls)];
491
541
  }
@@ -2769,7 +2819,8 @@ document.addEventListener('DOMContentLoaded', () => {
2769
2819
  }
2770
2820
  async runV3AgentWorkflow(message, context = {}) {
2771
2821
  const executionContext = await this.bindExecutionContext(context);
2772
- const baseTimeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
2822
+ const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
2823
+ const baseTimeoutMs = Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 ? requestedTimeoutMs : 0;
2773
2824
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
2774
2825
  const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
2775
2826
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
@@ -2777,7 +2828,7 @@ document.addEventListener('DOMContentLoaded', () => {
2777
2828
  && context.localMachineCapable !== false;
2778
2829
  const rescueEligibleSaaS = preferLocalV3
2779
2830
  && /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
2780
- const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
2831
+ const timeoutMs = baseTimeoutMs > 0 && rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
2781
2832
  const maxAttempts = preferLocalV3 ? 2 : 1;
2782
2833
  let lastErrors = [];
2783
2834
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
@@ -2811,7 +2862,7 @@ document.addEventListener('DOMContentLoaded', () => {
2811
2862
  };
2812
2863
  for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
2813
2864
  const controller = new AbortController();
2814
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2865
+ const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
2815
2866
  try {
2816
2867
  const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
2817
2868
  if (!response.ok) {
@@ -2843,7 +2894,7 @@ document.addEventListener('DOMContentLoaded', () => {
2843
2894
  stream: true,
2844
2895
  };
2845
2896
  const continueController = new AbortController();
2846
- const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
2897
+ const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
2847
2898
  try {
2848
2899
  const continueHeaders = await this.getV3AgentHeaders();
2849
2900
  const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
@@ -2861,7 +2912,8 @@ document.addEventListener('DOMContentLoaded', () => {
2861
2912
  break; // Fall through to normal completion with partial data
2862
2913
  }
2863
2914
  finally {
2864
- clearTimeout(continueTimeoutId);
2915
+ if (continueTimeoutId)
2916
+ clearTimeout(continueTimeoutId);
2865
2917
  }
2866
2918
  }
2867
2919
  // Use the final continuation data for workspace recovery
@@ -2915,7 +2967,8 @@ document.addEventListener('DOMContentLoaded', () => {
2915
2967
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
2916
2968
  }
2917
2969
  finally {
2918
- clearTimeout(timeoutId);
2970
+ if (timeoutId)
2971
+ clearTimeout(timeoutId);
2919
2972
  }
2920
2973
  }
2921
2974
  lastErrors = errors;
@@ -2982,7 +3035,10 @@ document.addEventListener('DOMContentLoaded', () => {
2982
3035
  }
2983
3036
  async runOperatorWorkflow(message, context = {}) {
2984
3037
  const executionContext = await this.bindExecutionContext(context);
2985
- const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
3038
+ const requestedOperatorTimeoutMs = Number(context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS);
3039
+ const timeoutMs = Number.isFinite(requestedOperatorTimeoutMs) && requestedOperatorTimeoutMs > 0
3040
+ ? requestedOperatorTimeoutMs
3041
+ : 0;
2986
3042
  const errors = [];
2987
3043
  const authToken = this.config.get('authToken');
2988
3044
  // Collect a lightweight workspace file listing so the operator can
@@ -2991,7 +3047,7 @@ document.addEventListener('DOMContentLoaded', () => {
2991
3047
  const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
2992
3048
  for (const baseUrl of this.getOperatorBaseUrls()) {
2993
3049
  const controller = new AbortController();
2994
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3050
+ const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
2995
3051
  try {
2996
3052
  const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
2997
3053
  method: 'POST',
@@ -3138,7 +3194,8 @@ document.addEventListener('DOMContentLoaded', () => {
3138
3194
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3139
3195
  }
3140
3196
  finally {
3141
- clearTimeout(timeoutId);
3197
+ if (timeoutId)
3198
+ clearTimeout(timeoutId);
3142
3199
  }
3143
3200
  }
3144
3201
  throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
@@ -3190,7 +3247,7 @@ document.addEventListener('DOMContentLoaded', () => {
3190
3247
  if (!this.shouldSkipCloudRoutes(resolvedModel)) {
3191
3248
  try {
3192
3249
  this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
3193
- const token = this.config.get('authToken');
3250
+ const token = this.getAccessToken();
3194
3251
  const response = await this.modelRouterClient.post('/v1/chat/completions', {
3195
3252
  model: resolvedModel,
3196
3253
  messages,
@@ -3254,6 +3311,35 @@ document.addEventListener('DOMContentLoaded', () => {
3254
3311
  const errMsg = error.response?.data?.error || error.message || 'Unknown error';
3255
3312
  this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
3256
3313
  }
3314
+ try {
3315
+ this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
3316
+ const token = this.getAccessToken();
3317
+ const response = await axios_1.default.post('https://coder.vigthoria.io/api/ai/chat', {
3318
+ messages,
3319
+ model: resolvedModel,
3320
+ maxTokens: this.config.get('preferences').maxTokens,
3321
+ temperature: 0.7,
3322
+ }, {
3323
+ timeout: 180000,
3324
+ httpsAgent: this._httpsAgent ?? undefined,
3325
+ headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
3326
+ });
3327
+ if (response.data?.success !== false) {
3328
+ const content = response.data.response || response.data.message || response.data.content;
3329
+ if (typeof content === 'string' && content.trim()) {
3330
+ return {
3331
+ id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
3332
+ message: content,
3333
+ model: response.data.model || resolvedModel || requestedModel,
3334
+ usage: response.data.usage,
3335
+ };
3336
+ }
3337
+ }
3338
+ }
3339
+ catch (error) {
3340
+ const errMsg = error.response?.data?.error || error.message || 'Unknown error';
3341
+ this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
3342
+ }
3257
3343
  }
3258
3344
  if (!preferSelfHostedFirst) {
3259
3345
  const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
@@ -3311,7 +3397,7 @@ document.addEventListener('DOMContentLoaded', () => {
3311
3397
  'vigthoria-cloud-ultra',
3312
3398
  ]);
3313
3399
  if (cloudModels.has(resolvedModel)) {
3314
- return 'vigthoria-v3-code-30b';
3400
+ return 'vigthoria-v3-code-35b';
3315
3401
  }
3316
3402
  return null;
3317
3403
  }
@@ -3326,6 +3412,12 @@ document.addEventListener('DOMContentLoaded', () => {
3326
3412
  return this.config.hasCloudAccess();
3327
3413
  }
3328
3414
  resolvePermittedModelId(shortName) {
3415
+ const normalizedRequested = String(shortName || '').trim().toLowerCase();
3416
+ const blockedModels = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
3417
+ if (blockedModels.has(normalizedRequested)) {
3418
+ this.logger.debug(`Blocked governed model ${shortName}; using fallback vigthoria-v3-code-35b`);
3419
+ return 'vigthoria-v3-code-35b';
3420
+ }
3329
3421
  const resolvedModel = this.resolveModelId(shortName);
3330
3422
  if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
3331
3423
  const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
@@ -3350,8 +3442,8 @@ document.addEventListener('DOMContentLoaded', () => {
3350
3442
  isSelfHostedPreferredModel(resolvedModel, requestedModel) {
3351
3443
  const normalizedRequested = String(requestedModel || '').toLowerCase();
3352
3444
  const selfHostedModels = new Set([
3353
- 'vigthoria-v3-code-30b',
3354
- 'vigthoria-v3-code-30b:latest',
3445
+ 'vigthoria-v3-code-35b',
3446
+ 'vigthoria-v3-code-35b:latest',
3355
3447
  'qwen3-coder:latest',
3356
3448
  'vigthoria-v2-code-8b',
3357
3449
  ]);
@@ -3363,9 +3455,9 @@ document.addEventListener('DOMContentLoaded', () => {
3363
3455
  }
3364
3456
  getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
3365
3457
  if (this.isSelfHostedPreferredModel(resolvedModel, requestedModel)) {
3366
- return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-30b' : resolvedModel;
3458
+ return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-35b' : resolvedModel;
3367
3459
  }
3368
- return 'vigthoria-v3-code-30b';
3460
+ return 'vigthoria-v3-code-35b';
3369
3461
  }
3370
3462
  // Streaming chat
3371
3463
  async *chatStream(messages, model) {
@@ -3448,7 +3540,7 @@ document.addEventListener('DOMContentLoaded', () => {
3448
3540
  // (/v1/chat/completions on api.vigthoria.io) which is the only
3449
3541
  // backend that reliably accepts our auth token.
3450
3542
  async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
3451
- const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-30b';
3543
+ const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-35b';
3452
3544
  const response = await this.modelRouterClient.post('/v1/chat/completions', {
3453
3545
  model: resolvedModel,
3454
3546
  messages: [
@@ -4301,15 +4393,16 @@ document.addEventListener('DOMContentLoaded', () => {
4301
4393
  'fast': 'vigthoria-fast-1.7b',
4302
4394
  'mini': 'vigthoria-mini-0.6b',
4303
4395
  'balanced': 'vigthoria-balanced-4b',
4396
+ 'balanced-4b': 'vigthoria-balanced-4b',
4304
4397
  'creative': 'vigthoria-creative-9b-v4',
4305
4398
  // 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',
4399
+ 'code': 'vigthoria-v3-code-35b', // Internal: self-hosted 35B on Blackwell
4400
+ 'code-30b': 'vigthoria-v3-code-35b',
4308
4401
  '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',
4402
+ 'pro': 'vigthoria-v3-code-35b',
4403
+ 'agent': 'vigthoria-v3-code-35b',
4404
+ 'vigthoria-code': 'vigthoria-v3-code-35b',
4405
+ 'vigthoria-agent': 'vigthoria-v3-code-35b',
4313
4406
  // ═══════════════════════════════════════════════════════════════
4314
4407
  // VIGTHORIA CLOUD - Premium cloud models (internal routing)
4315
4408
  // ═══════════════════════════════════════════════════════════════
@@ -4324,7 +4417,7 @@ document.addEventListener('DOMContentLoaded', () => {
4324
4417
  }
4325
4418
  return shortName;
4326
4419
  }
4327
- return modelMap[shortName] || 'vigthoria-v3-code-30b';
4420
+ return modelMap[shortName] || 'vigthoria-v3-code-35b';
4328
4421
  }
4329
4422
  async getCoderHealth() {
4330
4423
  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);