vigthoria-cli 1.13.20 → 1.13.23

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.
Files changed (187) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +63 -128
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +3 -42
  12. package/dist/commands/chat.js +378 -1123
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +23 -29
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +207 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +244 -198
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +368 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +124 -1589
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +44 -266
  71. package/dist/utils/api.js +647 -3180
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +220 -94
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +5 -6
  106. package/dist/utils/fastAgentRouter.js +17 -25
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +15 -0
  128. package/dist/utils/preview-screenshot-adapter.js +51 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/runtime-temp.d.ts +76 -0
  143. package/dist/utils/runtime-temp.js +492 -0
  144. package/dist/utils/secret-policy.d.ts +20 -0
  145. package/dist/utils/secret-policy.js +160 -0
  146. package/dist/utils/session.d.ts +17 -3
  147. package/dist/utils/session.js +354 -110
  148. package/dist/utils/tool-approval-service.d.ts +17 -0
  149. package/dist/utils/tool-approval-service.js +78 -0
  150. package/dist/utils/tool-capability-providers.d.ts +57 -0
  151. package/dist/utils/tool-capability-providers.js +57 -0
  152. package/dist/utils/tools.d.ts +2 -23
  153. package/dist/utils/tools.js +226 -512
  154. package/dist/utils/update-policy.d.ts +18 -0
  155. package/dist/utils/update-policy.js +137 -0
  156. package/dist/utils/v3-agent-client.d.ts +40 -0
  157. package/dist/utils/v3-agent-client.js +165 -0
  158. package/dist/utils/v3-workspace-service.d.ts +28 -0
  159. package/dist/utils/v3-workspace-service.js +279 -0
  160. package/dist/utils/vigflow-client.d.ts +95 -0
  161. package/dist/utils/vigflow-client.js +105 -0
  162. package/dist/utils/workspace-boundary.d.ts +45 -0
  163. package/dist/utils/workspace-boundary.js +377 -0
  164. package/dist/utils/workspace-brain-service.d.ts +2 -0
  165. package/dist/utils/workspace-brain-service.js +11 -1
  166. package/dist/utils/workspace-cache.js +37 -15
  167. package/dist/utils/workspace-stream.js +35 -10
  168. package/install.ps1 +115 -142
  169. package/install.sh +112 -81
  170. package/package.json +43 -9
  171. package/release-policy.json +43 -0
  172. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  173. package/scripts/release/generate-completions.mjs +24 -0
  174. package/scripts/release/install-release.mjs +10 -0
  175. package/scripts/release/publish-cli-release.mjs +116 -0
  176. package/scripts/release/publish-cli-release.sh +2 -70
  177. package/scripts/release/resolve-release-manifest.mjs +26 -0
  178. package/scripts/release/test-balanced-model-live.sh +45 -0
  179. package/scripts/release/validate-live-service-gates.sh +93 -0
  180. package/scripts/release/validate-no-go-gates.sh +145 -174
  181. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  182. package/dist/commands/index.d.ts +0 -12
  183. package/dist/commands/index.js +0 -194
  184. package/dist/commands/update.d.ts +0 -9
  185. package/dist/commands/update.js +0 -301
  186. package/dist/utils/templateInstantPath.d.ts +0 -18
  187. package/dist/utils/templateInstantPath.js +0 -228
@@ -0,0 +1,41 @@
1
+ import { Config } from './config.js';
2
+ export interface TokenClaims {
3
+ issuer: string;
4
+ audience: string[];
5
+ expiresAt: number;
6
+ subject?: string;
7
+ }
8
+ export interface ValidatedToken {
9
+ token: string;
10
+ type: 'jwt' | 'opaque';
11
+ claims?: TokenClaims;
12
+ }
13
+ export interface AuthIdentity {
14
+ userId?: string | null;
15
+ email?: string | null;
16
+ }
17
+ export interface AuthSessionOptions {
18
+ fetchImpl?: typeof fetch;
19
+ now?: () => number;
20
+ }
21
+ type AuthInvalidationListener = () => void;
22
+ export declare function subscribeAuthInvalidation(listener: AuthInvalidationListener): () => void;
23
+ export declare function validateTokenStructure(token: string, endpoint: string, now?: number): ValidatedToken;
24
+ export declare class AuthSessionService {
25
+ private readonly config;
26
+ private readonly fetchImpl;
27
+ private readonly now;
28
+ constructor(config: Config, options?: AuthSessionOptions);
29
+ getAccessToken(): string | null;
30
+ validateServerAcceptance(token: string, endpoint?: string): Promise<AuthIdentity>;
31
+ validateAndPersistToken(token: string, options?: {
32
+ apiUrl?: string;
33
+ refreshToken?: string | null;
34
+ identity?: AuthIdentity;
35
+ v3ServiceKey?: string | null;
36
+ }): Promise<ValidatedToken>;
37
+ refresh(): Promise<boolean>;
38
+ private refreshOnce;
39
+ clearLocalCredentials(): void;
40
+ }
41
+ export {};
@@ -0,0 +1,224 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { clearGatewayPreflightCache } from './cli-state.js';
4
+ import { assertTrustedEndpoint, guardedFetch } from './network-policy.js';
5
+ const invalidationListeners = new Set();
6
+ const refreshes = new Map();
7
+ export function subscribeAuthInvalidation(listener) {
8
+ invalidationListeners.add(listener);
9
+ return () => invalidationListeners.delete(listener);
10
+ }
11
+ function notifyAuthInvalidation() {
12
+ for (const listener of [...invalidationListeners]) {
13
+ try {
14
+ listener();
15
+ }
16
+ catch { /* one client must not block credential clearing */ }
17
+ }
18
+ }
19
+ function decodeBase64UrlJson(segment) {
20
+ if (!/^[A-Za-z0-9_-]+$/.test(segment))
21
+ throw new Error('JWT contains invalid base64url characters.');
22
+ const decoded = Buffer.from(segment, 'base64url').toString('utf8');
23
+ const value = JSON.parse(decoded);
24
+ if (!value || typeof value !== 'object' || Array.isArray(value))
25
+ throw new Error('JWT segment is not an object.');
26
+ return value;
27
+ }
28
+ function normalizeAudience(value) {
29
+ if (typeof value === 'string' && value.trim())
30
+ return [value.trim()];
31
+ if (Array.isArray(value))
32
+ return value.filter((item) => typeof item === 'string' && Boolean(item.trim())).map((item) => item.trim());
33
+ return [];
34
+ }
35
+ export function validateTokenStructure(token, endpoint, now = Date.now()) {
36
+ const normalized = String(token || '').trim();
37
+ if (!normalized || normalized.length > 16_384 || /[\s\u0000-\u001f\u007f]/.test(normalized)) {
38
+ throw new Error('Token is empty or contains invalid characters.');
39
+ }
40
+ const trustedEndpoint = assertTrustedEndpoint(endpoint, { audience: 'coder', credentialBearing: true, ignoreOffline: true });
41
+ const segments = normalized.split('.');
42
+ if (segments.length !== 3) {
43
+ if (normalized.length < 20)
44
+ throw new Error('Opaque token is too short.');
45
+ return { token: normalized, type: 'opaque' };
46
+ }
47
+ const header = decodeBase64UrlJson(segments[0]);
48
+ const payload = decodeBase64UrlJson(segments[1]);
49
+ if (!segments[2] || String(header.alg || '').toLowerCase() === 'none')
50
+ throw new Error('Unsigned JWTs are not accepted.');
51
+ const issuer = typeof payload.iss === 'string' ? payload.iss.trim().replace(/\/$/, '') : '';
52
+ const audience = normalizeAudience(payload.aud);
53
+ const expiresAt = typeof payload.exp === 'number' && Number.isFinite(payload.exp) ? payload.exp * 1000 : Number.NaN;
54
+ const notBefore = typeof payload.nbf === 'number' && Number.isFinite(payload.nbf) ? payload.nbf * 1000 : null;
55
+ if (!issuer)
56
+ throw new Error('JWT issuer claim is required.');
57
+ if (audience.length === 0)
58
+ throw new Error('JWT audience claim is required.');
59
+ if (!Number.isFinite(expiresAt))
60
+ throw new Error('JWT expiry claim is required.');
61
+ if (expiresAt <= now + 30_000)
62
+ throw new Error('Token is expired or too close to expiry.');
63
+ if (notBefore !== null && notBefore > now + 30_000)
64
+ throw new Error('Token is not active yet.');
65
+ const allowedIssuers = new Set([
66
+ trustedEndpoint.origin,
67
+ 'https://coder.vigthoria.io',
68
+ 'https://api.vigthoria.io',
69
+ ]);
70
+ if (!allowedIssuers.has(issuer))
71
+ throw new Error('Token issuer does not belong to the configured Vigthoria endpoint.');
72
+ const allowedAudiences = new Set([
73
+ 'vigthoria-cli',
74
+ 'vigthoria-coder',
75
+ 'coder',
76
+ trustedEndpoint.hostname,
77
+ trustedEndpoint.origin,
78
+ ]);
79
+ if (!audience.some((entry) => allowedAudiences.has(entry)))
80
+ throw new Error('Token audience is not valid for the Vigthoria CLI.');
81
+ return {
82
+ token: normalized,
83
+ type: 'jwt',
84
+ claims: {
85
+ issuer,
86
+ audience,
87
+ expiresAt,
88
+ subject: typeof payload.sub === 'string' ? payload.sub : undefined,
89
+ },
90
+ };
91
+ }
92
+ function extractIdentity(payload) {
93
+ if (!payload || typeof payload !== 'object')
94
+ return {};
95
+ const root = payload;
96
+ const user = root.user && typeof root.user === 'object' ? root.user : root;
97
+ return {
98
+ userId: user.id === undefined ? undefined : String(user.id),
99
+ email: typeof user.email === 'string' ? user.email : undefined,
100
+ };
101
+ }
102
+ export class AuthSessionService {
103
+ config;
104
+ fetchImpl;
105
+ now;
106
+ constructor(config, options = {}) {
107
+ this.config = config;
108
+ this.fetchImpl = options.fetchImpl || globalThis.fetch;
109
+ this.now = options.now || Date.now;
110
+ }
111
+ getAccessToken() {
112
+ return process.env.VIGTHORIA_TOKEN
113
+ || process.env.VIGTHORIA_AUTH_TOKEN
114
+ || this.config.get('authToken')
115
+ || null;
116
+ }
117
+ async validateServerAcceptance(token, endpoint = String(this.config.get('apiUrl'))) {
118
+ const base = assertTrustedEndpoint(endpoint, { audience: 'coder', credentialBearing: true }).origin;
119
+ validateTokenStructure(token, base, this.now());
120
+ const headers = {
121
+ Authorization: `Bearer ${token}`,
122
+ Cookie: `vigthoria-auth-token=${token}`,
123
+ Accept: 'application/json',
124
+ };
125
+ const probes = [
126
+ { path: '/api/auth/me', method: 'GET' },
127
+ { path: '/api/user/profile', method: 'GET' },
128
+ { path: '/api/user/info', method: 'GET' },
129
+ { path: '/api/user/subscription', method: 'GET' },
130
+ { path: '/api/auth/verify', method: 'POST' },
131
+ ];
132
+ let lastStatus = 0;
133
+ for (const probe of probes) {
134
+ const response = await guardedFetch(`${base}${probe.path}`, {
135
+ method: probe.method,
136
+ headers: probe.method === 'POST' ? { ...headers, 'Content-Type': 'application/json' } : headers,
137
+ body: probe.method === 'POST' ? JSON.stringify({ token }) : undefined,
138
+ signal: AbortSignal.timeout(8000),
139
+ }, { audience: 'coder', fetchImpl: this.fetchImpl });
140
+ lastStatus = response.status;
141
+ if (response.ok) {
142
+ const payload = await response.json().catch(() => ({}));
143
+ return extractIdentity(payload);
144
+ }
145
+ if (response.status === 401 || response.status === 403)
146
+ break;
147
+ if (response.status !== 404 && response.status !== 405)
148
+ break;
149
+ }
150
+ throw new Error(`Token was rejected by the configured Vigthoria endpoint${lastStatus ? ` (HTTP ${lastStatus})` : ''}.`);
151
+ }
152
+ async validateAndPersistToken(token, options = {}) {
153
+ const apiUrl = String(options.apiUrl || this.config.get('apiUrl'));
154
+ const validated = validateTokenStructure(token, apiUrl, this.now());
155
+ const acceptedIdentity = await this.validateServerAcceptance(validated.token, apiUrl);
156
+ this.config.setAuth({
157
+ token: validated.token,
158
+ refreshToken: options.refreshToken,
159
+ userId: acceptedIdentity.userId ?? options.identity?.userId,
160
+ email: acceptedIdentity.email ?? options.identity?.email,
161
+ apiUrl,
162
+ v3ServiceKey: options.v3ServiceKey,
163
+ });
164
+ clearGatewayPreflightCache();
165
+ notifyAuthInvalidation();
166
+ return validated;
167
+ }
168
+ async refresh() {
169
+ const key = this.config.getConfigPath();
170
+ const active = refreshes.get(key);
171
+ if (active)
172
+ return active;
173
+ const operation = this.refreshOnce().finally(() => refreshes.delete(key));
174
+ refreshes.set(key, operation);
175
+ return operation;
176
+ }
177
+ async refreshOnce() {
178
+ const refreshToken = this.config.get('refreshToken');
179
+ if (!refreshToken)
180
+ return false;
181
+ const base = assertTrustedEndpoint(String(this.config.get('apiUrl')), { audience: 'coder' }).origin;
182
+ try {
183
+ const response = await guardedFetch(`${base}/api/token/refresh`, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ refresh_token: refreshToken }),
187
+ signal: AbortSignal.timeout(8000),
188
+ }, { audience: 'coder', fetchImpl: this.fetchImpl });
189
+ if (!response.ok) {
190
+ if (response.status === 401 || response.status === 403)
191
+ this.clearLocalCredentials();
192
+ return false;
193
+ }
194
+ const payload = await response.json();
195
+ const nextToken = payload.token || payload.access_token;
196
+ if (!nextToken)
197
+ return false;
198
+ validateTokenStructure(nextToken, base, this.now());
199
+ this.config.setAuth({ token: nextToken, refreshToken: payload.refresh_token || refreshToken });
200
+ clearGatewayPreflightCache();
201
+ notifyAuthInvalidation();
202
+ return true;
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
208
+ clearLocalCredentials() {
209
+ this.config.clearAuth();
210
+ clearGatewayPreflightCache();
211
+ const compatibilityDir = path.dirname(this.config.getConfigPath());
212
+ for (const fileName of ['auth.json', 'session.json']) {
213
+ const candidate = path.join(compatibilityDir, fileName);
214
+ try {
215
+ fs.unlinkSync(candidate);
216
+ }
217
+ catch (error) {
218
+ if (error.code !== 'ENOENT')
219
+ throw error;
220
+ }
221
+ }
222
+ notifyAuthInvalidation();
223
+ }
224
+ }
@@ -1,7 +1,3 @@
1
- /**
2
- * Brain Hub client — sync workspace index + fetch account brain context
3
- * via coder.vigthoria.io (JWT-authenticated).
4
- */
5
1
  export type BrainSyncPayload = {
6
2
  workspaceName: string;
7
3
  workspacePath: string;
@@ -2,6 +2,8 @@
2
2
  * Brain Hub client — sync workspace index + fetch account brain context
3
3
  * via coder.vigthoria.io (JWT-authenticated).
4
4
  */
5
+ import { assertTrustedEndpoint } from './network-policy.js';
6
+ import { redactSensitiveText, scanOutboundContext } from './secret-policy.js';
5
7
  // Brain Hub calls are an optional personalization enhancement to an agent
6
8
  // turn, not on the critical path. Native `fetch()` has NO default timeout,
7
9
  // so a slow/unresponsive coder.vigthoria.io backend previously caused the
@@ -23,6 +25,7 @@ export class BrainHubClient {
23
25
  getAuthToken;
24
26
  constructor(options) {
25
27
  this.apiBase = (options.apiBase || 'https://coder.vigthoria.io').replace(/\/$/, '');
28
+ assertTrustedEndpoint(this.apiBase, { audience: 'brain-hub' });
26
29
  this.getAuthToken = options.getAuthToken;
27
30
  }
28
31
  async headers() {
@@ -42,7 +45,7 @@ export class BrainHubClient {
42
45
  const resp = await fetchWithTimeout(`${this.apiBase}/api/brain/sync-index`, {
43
46
  method: 'POST',
44
47
  headers,
45
- body: JSON.stringify(payload),
48
+ body: JSON.stringify(scanOutboundContext(payload).value),
46
49
  });
47
50
  const data = await resp.json().catch(() => ({}));
48
51
  if (!resp.ok) {
@@ -52,7 +55,7 @@ export class BrainHubClient {
52
55
  }
53
56
  catch (error) {
54
57
  const timedOut = error instanceof Error && error.name === 'AbortError';
55
- return { ok: false, error: timedOut ? `timeout after ${BRAIN_HUB_TIMEOUT_MS}ms` : String(error) };
58
+ return { ok: false, error: timedOut ? `timeout after ${BRAIN_HUB_TIMEOUT_MS}ms` : redactSensitiveText(String(error)) };
56
59
  }
57
60
  }
58
61
  async fetchAccountContext(limit = 25) {
@@ -70,7 +73,7 @@ export class BrainHubClient {
70
73
  }
71
74
  catch (error) {
72
75
  const timedOut = error instanceof Error && error.name === 'AbortError';
73
- return { ok: false, formattedText: '', error: timedOut ? `timeout after ${BRAIN_HUB_TIMEOUT_MS}ms` : String(error) };
76
+ return { ok: false, formattedText: '', error: timedOut ? `timeout after ${BRAIN_HUB_TIMEOUT_MS}ms` : redactSensitiveText(String(error)) };
74
77
  }
75
78
  }
76
79
  }
@@ -15,6 +15,7 @@ import WebSocket from 'ws';
15
15
  import * as os from 'os';
16
16
  import chalk from 'chalk';
17
17
  import { CH } from './logger.js';
18
+ import { assertTrustedWebSocketEndpoint } from './network-policy.js';
18
19
  // ── Singleton accessor ───────────────────────────────────────────────
19
20
  let _instance = null;
20
21
  /** Get the active bridge client (may be null if --bridge was not used). */
@@ -41,6 +42,7 @@ export class BridgeClient {
41
42
  if (!this.url.includes('/ws')) {
42
43
  this.url = this.url.replace(/^http/, 'ws') + '/ws';
43
44
  }
45
+ assertTrustedWebSocketEndpoint(this.url, 'bridge', Boolean(opts.apiKey));
44
46
  this.apiKey = opts.apiKey;
45
47
  this.machineLabel = opts.machineLabel || os.hostname();
46
48
  this.clientId = `cli-${this.machineLabel}-${Date.now().toString(36)}`;
@@ -0,0 +1,48 @@
1
+ export interface EndpointHealthStatus {
2
+ name: string;
3
+ endpoint: string;
4
+ ok: boolean;
5
+ error?: string;
6
+ details?: Record<string, unknown>;
7
+ }
8
+ export interface APIHealthStatus {
9
+ overallOk: boolean;
10
+ coder: EndpointHealthStatus;
11
+ models: EndpointHealthStatus;
12
+ selfHosted: EndpointHealthStatus | null;
13
+ }
14
+ export interface CapabilityTruthStatus {
15
+ overallOk: boolean;
16
+ v3Agent: EndpointHealthStatus;
17
+ hyperLoop: EndpointHealthStatus;
18
+ repoMemory: EndpointHealthStatus;
19
+ devtoolsBridge: EndpointHealthStatus;
20
+ }
21
+ export type ModelPreflightStatus = {
22
+ healthy: boolean;
23
+ endpoint: string;
24
+ error?: string;
25
+ routes: Array<{
26
+ name: string;
27
+ ok: boolean;
28
+ error?: string;
29
+ }>;
30
+ };
31
+ export interface CapabilityHealthDependencies {
32
+ getCoderHealth(): Promise<EndpointHealthStatus>;
33
+ getModelsHealth(): Promise<EndpointHealthStatus>;
34
+ getSelfHostedHealth(): Promise<EndpointHealthStatus | null>;
35
+ getV3Health(): Promise<EndpointHealthStatus>;
36
+ getHyperLoopHealth(): Promise<EndpointHealthStatus>;
37
+ getRepoMemoryHealth(context: Record<string, any>): Promise<EndpointHealthStatus>;
38
+ getDevtoolsBridgeHealth(): Promise<EndpointHealthStatus>;
39
+ modelUsesV3(requestedModel: string): boolean;
40
+ }
41
+ export declare class CapabilityHealthService {
42
+ private readonly dependencies;
43
+ constructor(dependencies: CapabilityHealthDependencies);
44
+ runModelPreflight(requestedModel?: string): Promise<ModelPreflightStatus>;
45
+ mapPreflightEndpoint(endpoint: string): 'coder' | 'models' | 'selfhosted' | null;
46
+ getCapabilityTruth(context?: Record<string, any>): Promise<CapabilityTruthStatus>;
47
+ getHealth(): Promise<APIHealthStatus>;
48
+ }
@@ -0,0 +1,64 @@
1
+ export class CapabilityHealthService {
2
+ dependencies;
3
+ constructor(dependencies) {
4
+ this.dependencies = dependencies;
5
+ }
6
+ async runModelPreflight(requestedModel = 'agent') {
7
+ const probes = [
8
+ { name: 'Vigthoria Coder API', run: () => this.dependencies.getCoderHealth() },
9
+ { name: 'Vigthoria Models API', run: () => this.dependencies.getModelsHealth() },
10
+ ];
11
+ const selfHosted = await this.dependencies.getSelfHostedHealth();
12
+ if (selfHosted)
13
+ probes.unshift({ name: selfHosted.name || 'Dedicated Models API', run: async () => selfHosted });
14
+ if (this.dependencies.modelUsesV3(requestedModel))
15
+ probes.push({ name: 'V3 Agent API', run: () => this.dependencies.getV3Health() });
16
+ const routes = await Promise.all(probes.map(async (probe) => {
17
+ try {
18
+ const status = await probe.run();
19
+ return { name: probe.name, ok: status.ok, error: status.error };
20
+ }
21
+ catch (error) {
22
+ return { name: probe.name, ok: false, error: error instanceof Error ? error.message : String(error) };
23
+ }
24
+ }));
25
+ const healthy = routes.some((route) => route.ok);
26
+ const inferenceRoutes = routes.filter((route) => route.name !== 'V3 Agent API');
27
+ const firstHealthy = inferenceRoutes.find((route) => route.ok) || routes.find((route) => route.ok);
28
+ return {
29
+ healthy,
30
+ endpoint: firstHealthy?.name || 'api.vigthoria.io',
31
+ error: healthy ? undefined : 'No Vigthoria model backend responded during preflight. Run `vigthoria login` and check your internet connection.',
32
+ routes,
33
+ };
34
+ }
35
+ mapPreflightEndpoint(endpoint) {
36
+ const normalized = String(endpoint || '').toLowerCase();
37
+ if (normalized.includes('coder'))
38
+ return 'coder';
39
+ if (normalized.includes('models') && !normalized.includes('dedicated'))
40
+ return 'models';
41
+ if (normalized.includes('dedicated') || normalized.includes('self-hosted') || normalized.includes('selfhosted'))
42
+ return 'selfhosted';
43
+ return null;
44
+ }
45
+ async getCapabilityTruth(context = {}) {
46
+ const withTimeout = (promise, name) => Promise.race([
47
+ promise,
48
+ new Promise((resolve) => setTimeout(() => resolve({ name, endpoint: '', ok: false, error: 'Service not reachable (15 s timeout)' }), 15000)),
49
+ ]);
50
+ const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
51
+ withTimeout(this.dependencies.getV3Health(), 'V3 Agent'),
52
+ withTimeout(this.dependencies.getHyperLoopHealth(), 'Hyper Loop'),
53
+ withTimeout(this.dependencies.getRepoMemoryHealth(context), 'Repo Memory'),
54
+ withTimeout(this.dependencies.getDevtoolsBridgeHealth(), 'DevTools Bridge'),
55
+ ]);
56
+ return { overallOk: v3Agent.ok && (hyperLoop.ok || repoMemory.ok), v3Agent, hyperLoop, repoMemory, devtoolsBridge };
57
+ }
58
+ async getHealth() {
59
+ const [coder, models, selfHosted] = await Promise.all([
60
+ this.dependencies.getCoderHealth(), this.dependencies.getModelsHealth(), this.dependencies.getSelfHostedHealth(),
61
+ ]);
62
+ return { overallOk: coder.ok && models.ok, coder, models, selfHosted };
63
+ }
64
+ }
@@ -0,0 +1,18 @@
1
+ export interface ChatRuntimeEnvironment {
2
+ platform: 'windows' | 'macos' | 'linux' | 'unknown';
3
+ shell?: string;
4
+ pathStyle?: 'windows' | 'posix';
5
+ cwd?: string;
6
+ [key: string]: unknown;
7
+ }
8
+ export declare class ChatPromptPolicy {
9
+ private readonly getRuntime;
10
+ constructor(getRuntime: () => ChatRuntimeEnvironment);
11
+ isDiagnostic(prompt: string): boolean;
12
+ isAnalysisLookup(prompt: string): boolean;
13
+ isBrowserTask(prompt: string): boolean;
14
+ isRepoGrounded(prompt: string): boolean;
15
+ isOperatorDirectAnswerable(prompt: string): boolean;
16
+ buildShapingInstructions(prompt: string): string;
17
+ buildExecutionPrompt(prompt: string): string;
18
+ }
@@ -0,0 +1,61 @@
1
+ import { taskRequiresWorkspaceChanges } from './requestIntent.js';
2
+ export class ChatPromptPolicy {
3
+ getRuntime;
4
+ constructor(getRuntime) {
5
+ this.getRuntime = getRuntime;
6
+ }
7
+ isDiagnostic(prompt) {
8
+ if (taskRequiresWorkspaceChanges(prompt))
9
+ return false;
10
+ return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
11
+ }
12
+ isAnalysisLookup(prompt) {
13
+ if (taskRequiresWorkspaceChanges(prompt))
14
+ return false;
15
+ const text = prompt.trim();
16
+ return /^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(text)
17
+ || /\b(where\s+(?:is|are)|what\s+(?:is|are|have|did|was|were)|how\s+(?:do|does|is|are)|what\s+(?:have\s+)?you\s+(?:done|changed|built)|what\s+(?:is|was)\s+(?:your|the)\s+answer)\b/i.test(text);
18
+ }
19
+ isBrowserTask(prompt) {
20
+ return /(\bbrowser\b|\bchrome\b|\bdevtools\b|\bconsole\b|\bdom\b|network tab|network request|frontend runtime|client-side|client side|rendering|page load|\bwebsocket\b|ui bug|inspect element)/i.test(prompt);
21
+ }
22
+ isRepoGrounded(prompt) {
23
+ const text = prompt.trim();
24
+ return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b|\.yaml\b|\.yml\b)/i.test(text)
25
+ || /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(text)
26
+ || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(text)
27
+ || /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(text)
28
+ || /\/[a-zA-Z]/.test(text)
29
+ || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(text)
30
+ || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(text);
31
+ }
32
+ isOperatorDirectAnswerable(prompt) {
33
+ const text = prompt.trim();
34
+ if (/^(status(\s+check)?|health(\s+check)?|ping)$/i.test(text))
35
+ return true;
36
+ if (this.isRepoGrounded(text))
37
+ return false;
38
+ return text.length < 120 && /^(reply with|say|echo|respond with|return|ping|hello|hi|test|status|what is \d|how much is)\b/i.test(text);
39
+ }
40
+ buildShapingInstructions(prompt) {
41
+ const instructions = [];
42
+ const runtime = this.getRuntime();
43
+ if (runtime.platform === 'windows')
44
+ instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
45
+ else if (runtime.platform === 'macos')
46
+ instructions.push('Platform: macOS. Prefer list_dir, grep, and read_file tools for deterministic results.', 'Use workspace-relative paths and verify file existence before claiming a file is missing.');
47
+ else if (runtime.platform === 'linux')
48
+ instructions.push('Platform: Linux. Prefer list_dir, grep, and read_file for grounded repository inspection.', 'Use exact file paths from tool output when reporting findings or fixes.');
49
+ if (this.isAnalysisLookup(prompt))
50
+ instructions.push('Read-only analysis mode is active.', 'Inspect the workspace with tools and produce a grounded report of findings, gaps, and blockers.', 'Do not create, modify, or delete files unless the user explicitly asked for changes.');
51
+ if (this.isDiagnostic(prompt))
52
+ instructions.push('Diagnostic mode is active.', 'Treat this as a debugging task, not a generic code review or feature request.', 'Start with concrete evidence: logs, runtime errors, config, launch files, and exact symbol references.', 'If log files exist, inspect them before proposing fixes.', 'Do not claim a file, definition, asset, or symbol is missing until you verify that with tools.', 'Prefer grep plus read_file around the exact references involved in the failure.', 'Separate your reasoning into: Evidence, Confirmed Cause, and Remaining Hypotheses.', 'CRITICAL GROUNDING RULE: Every key name, variable name, symbol, or identifier in the final answer must appear verbatim in tool evidence.', 'CROSS-FILE ATTRIBUTION: Report a key or symbol as conflicting only when tool evidence shows it in both files.', 'Cite exact file, line, and string evidence for each reported conflict.');
53
+ if (this.isBrowserTask(prompt))
54
+ instructions.push('Browser-debug mode is active.', 'Prefer concrete browser evidence such as console errors, network failures, DOM state, and websocket behavior.', 'Use the DevTools Bridge only when the user explicitly enabled it for this run.');
55
+ return instructions.join('\n');
56
+ }
57
+ buildExecutionPrompt(prompt) {
58
+ const shaping = this.buildShapingInstructions(prompt);
59
+ return shaping ? `${prompt}\n\n${shaping}` : prompt;
60
+ }
61
+ }
@@ -0,0 +1,11 @@
1
+ export interface ParsedToolCall {
2
+ tool: string;
3
+ args: Record<string, string>;
4
+ }
5
+ export declare class ChatToolCallParser {
6
+ extract(message: string): ParsedToolCall[];
7
+ normalizeName(name: string): string;
8
+ parseLegacy(message: string): ParsedToolCall[];
9
+ parsePayload(payload: string): ParsedToolCall | null;
10
+ strip(message: string): string;
11
+ }
@@ -0,0 +1,71 @@
1
+ export class ChatToolCallParser {
2
+ extract(message) {
3
+ const calls = [];
4
+ const seen = new Set();
5
+ const add = (call) => {
6
+ if (!call)
7
+ return;
8
+ const key = `${call.tool}::${JSON.stringify(call.args)}`;
9
+ if (!seen.has(key)) {
10
+ seen.add(key);
11
+ calls.push(call);
12
+ }
13
+ };
14
+ for (const match of message.matchAll(/<tool_call>([\s\S]*?)<\/tool_call>/g))
15
+ add(this.parsePayload(match[1] || ''));
16
+ for (const call of this.parseLegacy(message))
17
+ add(call);
18
+ return calls;
19
+ }
20
+ normalizeName(name) {
21
+ const normalized = name.trim().toLowerCase();
22
+ const aliases = {
23
+ list_directory: 'list_dir', listdirectory: 'list_dir', ls: 'list_dir', dir: 'list_dir',
24
+ readfile: 'read_file', writefile: 'write_file', editfile: 'edit_file',
25
+ shell: 'bash', command: 'bash', run_command: 'bash',
26
+ };
27
+ return aliases[normalized] || normalized;
28
+ }
29
+ parseLegacy(message) {
30
+ const calls = [];
31
+ const functionRegex = /<function\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?:<\/function>|$)/gi;
32
+ for (const match of message.matchAll(functionRegex)) {
33
+ const tool = this.normalizeName(match[1] || '');
34
+ if (!tool)
35
+ continue;
36
+ const args = {};
37
+ const parameterRegex = /<parameter\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?=<parameter\s*=|<\/function>|<\/parameter>|$)/gi;
38
+ for (const parameter of (match[2] || '').matchAll(parameterRegex)) {
39
+ if (parameter[1])
40
+ args[parameter[1]] = (parameter[2] || '').replace(/<\/parameter>\s*$/i, '').trim();
41
+ }
42
+ calls.push({ tool, args });
43
+ }
44
+ return calls;
45
+ }
46
+ parsePayload(payload) {
47
+ const normalized = payload.trim();
48
+ if (!normalized)
49
+ return null;
50
+ try {
51
+ const parsed = JSON.parse(normalized);
52
+ if (typeof parsed.tool !== 'string')
53
+ return null;
54
+ const args = {};
55
+ if (parsed.args && typeof parsed.args === 'object') {
56
+ for (const [key, value] of Object.entries(parsed.args))
57
+ args[key] = typeof value === 'string' ? value : JSON.stringify(value);
58
+ }
59
+ return { tool: this.normalizeName(parsed.tool), args };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ strip(message) {
66
+ return message
67
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
68
+ .replace(/<function\s*=\s*["']?[A-Za-z0-9_-]+["']?>[\s\S]*?(?:<\/function>|$)/gi, '')
69
+ .trim();
70
+ }
71
+ }
@@ -21,13 +21,30 @@ export interface CliStateShape {
21
21
  latestVersion: string;
22
22
  notifiedFor?: string;
23
23
  };
24
- gatewayPreflight?: {
24
+ gatewayPreflight?: Record<string, {
25
25
  checkedAt: number;
26
26
  valid: boolean;
27
- };
27
+ endpoint: string;
28
+ credentialFingerprint: string;
29
+ capability: string;
30
+ configurationFingerprint: string;
31
+ }>;
28
32
  /** Last-seen version — used to render release notes on upgrade. */
29
33
  lastSeenVersion?: string;
30
34
  }
35
+ export interface GatewayPreflightCacheKeyInput {
36
+ endpoint: string;
37
+ credential: string;
38
+ capability: string;
39
+ relevantConfiguration?: unknown;
40
+ }
41
+ export interface GatewayPreflightCacheKey {
42
+ id: string;
43
+ endpoint: string;
44
+ credentialFingerprint: string;
45
+ capability: string;
46
+ configurationFingerprint: string;
47
+ }
31
48
  export declare function ensureStateDir(): void;
32
49
  /** Best-effort lock-down: `0o600` on POSIX, no-op on Windows (NTFS ACL ≠ chmod). */
33
50
  export declare function secureFileMode(filePath: string): void;
@@ -49,8 +66,9 @@ export declare function isUpdateCheckSuppressed(): boolean;
49
66
  /** Cached read for the latest known version — null when stale or absent. */
50
67
  export declare function readCachedLatestVersion(maxAgeMs?: number): string | null;
51
68
  export declare function writeCachedLatestVersion(latestVersion: string, notifiedFor?: string): void;
52
- export declare function readGatewayPreflightCache(maxAgeMs?: number): boolean | null;
53
- export declare function writeGatewayPreflightCache(valid: boolean): void;
69
+ export declare function createGatewayPreflightCacheKey(input: GatewayPreflightCacheKeyInput): GatewayPreflightCacheKey;
70
+ export declare function readGatewayPreflightCache(key: GatewayPreflightCacheKey, maxAgeMs?: number): boolean | null;
71
+ export declare function writeGatewayPreflightCache(key: GatewayPreflightCacheKey, valid: boolean): void;
54
72
  /**
55
73
  * Invalidate any cached gateway-preflight result. Must be called on
56
74
  * every login/logout/token change so a stale cached `false` from a