vigthoria-cli 1.13.23 → 1.13.25
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/completions/_vigthoria +1 -1
- package/completions/vigthoria.fish +1 -1
- package/dist/commands/auth.js +69 -17
- package/dist/commands/chat.js +41 -1
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +33 -9
- package/dist/commands/game.d.ts +4 -0
- package/dist/commands/game.js +19 -1
- package/dist/commands/hub.d.ts +2 -0
- package/dist/commands/hub.js +61 -78
- package/dist/commands/legion.d.ts +1 -0
- package/dist/commands/legion.js +7 -7
- package/dist/commands/platform-registration.js +1 -1
- package/dist/commands/product-run-registration.js +4 -2
- package/dist/commands/repo.d.ts +2 -0
- package/dist/commands/repo.js +37 -0
- package/dist/commands/security.d.ts +3 -0
- package/dist/commands/security.js +29 -9
- package/dist/index.js +16 -2
- package/dist/utils/api.js +14 -11
- package/dist/utils/chat-prompt-policy.js +1 -1
- package/dist/utils/code-operations-service.js +43 -14
- package/dist/utils/config.js +5 -2
- package/dist/utils/local-security-service.d.ts +23 -0
- package/dist/utils/local-security-service.js +210 -0
- package/dist/utils/model-transport-service.js +66 -6
- package/dist/utils/network-policy.d.ts +1 -1
- package/dist/utils/network-policy.js +3 -2
- package/dist/utils/preview-screenshot-adapter.js +2 -2
- package/dist/utils/secret-policy.js +27 -18
- package/dist/utils/subscription-policy.d.ts +11 -0
- package/dist/utils/subscription-policy.js +32 -0
- package/dist/utils/v3-workspace-service.js +5 -1
- package/dist/utils/vigflow-client.js +2 -2
- package/package.json +3 -11
- package/release-policy.json +2 -1
- package/scripts/release/generate-release-evidence.mjs +49 -0
- package/scripts/release/publish-cli-release.mjs +19 -8
- package/scripts/release/validate-no-go-gates.sh +2 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isSensitivePath } from './secret-policy.js';
|
|
5
|
+
import { WorkspaceBoundaryError } from './workspace-boundary.js';
|
|
6
|
+
const MAX_FILES = 5_000;
|
|
7
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
const SCANNABLE_EXTENSIONS = new Set([
|
|
10
|
+
'.cjs', '.css', '.go', '.html', '.htm', '.java', '.js', '.jsx', '.json',
|
|
11
|
+
'.mjs', '.php', '.ps1', '.py', '.rb', '.sh', '.sql', '.ts', '.tsx',
|
|
12
|
+
'.vue', '.xml', '.yaml', '.yml',
|
|
13
|
+
]);
|
|
14
|
+
const EXCLUDED_DIRECTORIES = new Set([
|
|
15
|
+
'.git', '.hg', '.svn', '.vigthoria', 'build', 'coverage', 'dist',
|
|
16
|
+
'node_modules', 'target', 'vendor', '__pycache__', '.venv', 'venv',
|
|
17
|
+
]);
|
|
18
|
+
// These are intentionally high-confidence source patterns. The scanner does
|
|
19
|
+
// not classify ordinary bind addresses, DOM APIs, or configuration examples as
|
|
20
|
+
// vulnerabilities without the context required to prove exploitability.
|
|
21
|
+
const RULES = [
|
|
22
|
+
{
|
|
23
|
+
id: 'hardcoded-private-key',
|
|
24
|
+
severity: 'critical',
|
|
25
|
+
expression: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g,
|
|
26
|
+
message: 'Private key material is embedded in source',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'hardcoded-access-token',
|
|
30
|
+
severity: 'critical',
|
|
31
|
+
expression: /\b(?:ghp|gho|ghu|ghs|github_pat|xox[baprs])_[A-Za-z0-9_-]{20,}\b/g,
|
|
32
|
+
message: 'A high-confidence access token is embedded in source',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'hardcoded-cloud-key',
|
|
36
|
+
severity: 'critical',
|
|
37
|
+
expression: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
38
|
+
message: 'An AWS access key identifier is embedded in source',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'child-process-shell',
|
|
42
|
+
severity: 'high',
|
|
43
|
+
expression: /\b(?:exec|execSync)\s*\(\s*(?:`|[^'"\s][^,\n]*)/g,
|
|
44
|
+
message: 'A child process appears to execute dynamically constructed shell text',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'dynamic-code-evaluation',
|
|
48
|
+
severity: 'high',
|
|
49
|
+
expression: /\b(?:eval|Function)\s*\(\s*(??['"])/g,
|
|
50
|
+
message: 'Dynamic code evaluation requires manual security review',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'unsafe-tls-disable',
|
|
54
|
+
severity: 'high',
|
|
55
|
+
expression: /\b(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|rejectUnauthorized\s*:\s*false)\b/g,
|
|
56
|
+
message: 'TLS certificate validation is explicitly disabled',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
function assertScanRoot(input) {
|
|
60
|
+
const resolved = path.resolve(input || process.cwd());
|
|
61
|
+
if (process.platform === 'win32' && /^(?:\\\\[?.]\\|\\\\(?!localhost\\))/i.test(resolved)) {
|
|
62
|
+
throw new WorkspaceBoundaryError('UNC and Windows device paths are not valid security scan roots', 'UNSAFE_WINDOWS_PATH');
|
|
63
|
+
}
|
|
64
|
+
const stat = fs.lstatSync(resolved);
|
|
65
|
+
if (stat.isSymbolicLink()) {
|
|
66
|
+
throw new WorkspaceBoundaryError('A symbolic link or junction cannot be used as a security scan root', 'WORKSPACE_LINK_ESCAPE');
|
|
67
|
+
}
|
|
68
|
+
if (!stat.isDirectory())
|
|
69
|
+
throw new WorkspaceBoundaryError('Security scan root must be a directory', 'WORKSPACE_PATH_INVALID');
|
|
70
|
+
return fs.realpathSync(resolved);
|
|
71
|
+
}
|
|
72
|
+
function stableIssueId(rule, file, line) {
|
|
73
|
+
return `SEC-${createHash('sha256').update(`${rule}\0${file}\0${line}`).digest('hex').slice(0, 12).toUpperCase()}`;
|
|
74
|
+
}
|
|
75
|
+
function lineAt(content, offset) {
|
|
76
|
+
let line = 1;
|
|
77
|
+
for (let index = 0; index < offset; index += 1)
|
|
78
|
+
if (content.charCodeAt(index) === 10)
|
|
79
|
+
line += 1;
|
|
80
|
+
return line;
|
|
81
|
+
}
|
|
82
|
+
function scoreFromIssues(issues) {
|
|
83
|
+
const penalty = { critical: 20, high: 10, medium: 4, low: 1 };
|
|
84
|
+
return Math.max(0, 100 - issues.reduce((sum, issue) => sum + penalty[issue.severity], 0));
|
|
85
|
+
}
|
|
86
|
+
function gradeFromScore(score) {
|
|
87
|
+
if (score >= 90)
|
|
88
|
+
return 'A';
|
|
89
|
+
if (score >= 75)
|
|
90
|
+
return 'B';
|
|
91
|
+
if (score >= 50)
|
|
92
|
+
return 'C';
|
|
93
|
+
if (score >= 30)
|
|
94
|
+
return 'D';
|
|
95
|
+
return 'F';
|
|
96
|
+
}
|
|
97
|
+
export function scanLocalWorkspace(inputRoot = process.cwd()) {
|
|
98
|
+
const rootDir = assertScanRoot(inputRoot);
|
|
99
|
+
const stack = [rootDir];
|
|
100
|
+
const issues = [];
|
|
101
|
+
let scannedFiles = 0;
|
|
102
|
+
let scannedBytes = 0;
|
|
103
|
+
let skippedFiles = 0;
|
|
104
|
+
while (stack.length > 0) {
|
|
105
|
+
const directory = stack.pop();
|
|
106
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const absolute = path.join(directory, entry.name);
|
|
109
|
+
const relative = path.relative(rootDir, absolute).split(path.sep).join('/');
|
|
110
|
+
if (entry.isSymbolicLink()) {
|
|
111
|
+
skippedFiles += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
if (!EXCLUDED_DIRECTORIES.has(entry.name) && !isSensitivePath(relative))
|
|
116
|
+
stack.push(absolute);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!entry.isFile()) {
|
|
120
|
+
skippedFiles += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (isSensitivePath(relative)) {
|
|
124
|
+
const basename = path.basename(relative).toLowerCase();
|
|
125
|
+
if (basename === '.env' || basename.startsWith('.env.')) {
|
|
126
|
+
issues.push({
|
|
127
|
+
id: stableIssueId('sensitive-env-file', relative, 1),
|
|
128
|
+
rule: 'sensitive-env-file',
|
|
129
|
+
severity: 'medium',
|
|
130
|
+
file: relative,
|
|
131
|
+
line: 1,
|
|
132
|
+
message: 'Sensitive environment file exists in the scanned tree; verify it is excluded from version control and uploads',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
skippedFiles += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
|
|
139
|
+
continue;
|
|
140
|
+
if (scannedFiles >= MAX_FILES)
|
|
141
|
+
throw new Error(`Security scan file limit exceeded (${MAX_FILES})`);
|
|
142
|
+
const stat = fs.statSync(absolute);
|
|
143
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
144
|
+
skippedFiles += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (scannedBytes + stat.size > MAX_TOTAL_BYTES)
|
|
148
|
+
throw new Error(`Security scan byte limit exceeded (${MAX_TOTAL_BYTES})`);
|
|
149
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
150
|
+
scannedFiles += 1;
|
|
151
|
+
scannedBytes += stat.size;
|
|
152
|
+
for (const rule of RULES) {
|
|
153
|
+
rule.expression.lastIndex = 0;
|
|
154
|
+
let match;
|
|
155
|
+
while ((match = rule.expression.exec(content)) !== null) {
|
|
156
|
+
const line = lineAt(content, match.index);
|
|
157
|
+
issues.push({
|
|
158
|
+
id: stableIssueId(rule.id, relative, line),
|
|
159
|
+
rule: rule.id,
|
|
160
|
+
severity: rule.severity,
|
|
161
|
+
file: relative,
|
|
162
|
+
line,
|
|
163
|
+
message: rule.message,
|
|
164
|
+
});
|
|
165
|
+
if (match[0].length === 0)
|
|
166
|
+
rule.expression.lastIndex += 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
issues.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.rule.localeCompare(right.rule));
|
|
172
|
+
const score = scoreFromIssues(issues);
|
|
173
|
+
return {
|
|
174
|
+
rootDir,
|
|
175
|
+
scannedFiles,
|
|
176
|
+
scannedBytes,
|
|
177
|
+
skippedFiles,
|
|
178
|
+
score,
|
|
179
|
+
grade: gradeFromScore(score),
|
|
180
|
+
issueCount: issues.length,
|
|
181
|
+
issues,
|
|
182
|
+
bounded: true,
|
|
183
|
+
localOnly: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function localSecurityFixPlan(result, issueId, requestedApply = false) {
|
|
187
|
+
const selected = issueId ? result.issues.filter((issue) => issue.id === issueId) : result.issues;
|
|
188
|
+
if (issueId && selected.length === 0)
|
|
189
|
+
throw new Error(`Security issue was not found in the current scan: ${issueId}`);
|
|
190
|
+
const actions = selected.slice(0, 100).map((issue) => ({
|
|
191
|
+
issue_id: issue.id,
|
|
192
|
+
file: issue.file,
|
|
193
|
+
line: issue.line,
|
|
194
|
+
rule: issue.rule,
|
|
195
|
+
action: issue.rule === 'sensitive-env-file'
|
|
196
|
+
? 'Confirm the file is ignored and excluded from every upload/context boundary; rotate any exposed values'
|
|
197
|
+
: 'Review the exact source location and replace the unsafe construct without weakening validation or trust boundaries',
|
|
198
|
+
}));
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
applied: false,
|
|
202
|
+
requestedApply,
|
|
203
|
+
confirmRequired: false,
|
|
204
|
+
plannedActions: actions,
|
|
205
|
+
message: requestedApply
|
|
206
|
+
? 'No mutation was performed: local security fixes require an explicit reviewed source patch.'
|
|
207
|
+
: 'Local security fix plan generated; no files were changed.',
|
|
208
|
+
localOnly: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanOutboundContext } from './secret-policy.js';
|
|
1
|
+
import { redactSensitiveText, scanOutboundContext } from './secret-policy.js';
|
|
2
2
|
export class ModelTransportService {
|
|
3
3
|
dependencies;
|
|
4
4
|
lastErrors = [];
|
|
@@ -28,15 +28,75 @@ export class ModelTransportService {
|
|
|
28
28
|
}
|
|
29
29
|
async complete(systemPrompt, userPrompt, model, maxTokens) {
|
|
30
30
|
const resolvedModel = model ? this.dependencies.resolvePermittedModelId(model) : 'Vigthoria-v4-Code-27B';
|
|
31
|
-
const
|
|
31
|
+
const messages = scanOutboundContext([{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }]).value;
|
|
32
|
+
const body = {
|
|
32
33
|
model: resolvedModel,
|
|
33
|
-
messages
|
|
34
|
+
messages,
|
|
34
35
|
max_tokens: maxTokens || this.dependencies.getMaxTokens() || 4096,
|
|
35
36
|
temperature: 0.3,
|
|
36
37
|
stream: false,
|
|
37
|
-
}
|
|
38
|
-
const
|
|
39
|
-
|
|
38
|
+
};
|
|
39
|
+
const failures = [];
|
|
40
|
+
const safeFailure = (value) => redactSensitiveText(String(value || 'transport failed')).slice(0, 120);
|
|
41
|
+
const extract = (response) => {
|
|
42
|
+
const content = response?.choices?.[0]?.message?.content
|
|
43
|
+
|| response?.choices?.[0]?.text
|
|
44
|
+
|| response?.response
|
|
45
|
+
|| response?.message
|
|
46
|
+
|| response?.content;
|
|
47
|
+
return response?.success === false || typeof content !== 'string' ? '' : content.trim();
|
|
48
|
+
};
|
|
49
|
+
if (!this.dependencies.shouldSkipCloudRoutes(resolvedModel) && !this.dependencies.isCloudModelId(resolvedModel)) {
|
|
50
|
+
try {
|
|
51
|
+
const content = extract(await this.dependencies.postModels(body));
|
|
52
|
+
if (content)
|
|
53
|
+
return content;
|
|
54
|
+
failures.push('models:empty response');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
failures.push(`models:${safeFailure(error?.response?.data?.error || error?.message)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (this.dependencies.isAuthenticated() && !this.dependencies.shouldSkipCloudRoutes(resolvedModel)) {
|
|
61
|
+
const coderBody = {
|
|
62
|
+
messages,
|
|
63
|
+
model: resolvedModel,
|
|
64
|
+
maxTokens: body.max_tokens,
|
|
65
|
+
temperature: body.temperature,
|
|
66
|
+
cloudConsent: this.dependencies.isCloudModelId(resolvedModel),
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const content = extract(await this.dependencies.postCoder(coderBody));
|
|
70
|
+
if (content)
|
|
71
|
+
return content;
|
|
72
|
+
failures.push('coder:empty response');
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
failures.push(`coder:${safeFailure(error?.response?.data?.error || error?.message)}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const content = extract(await this.dependencies.postCanonicalCoder(coderBody));
|
|
79
|
+
if (content)
|
|
80
|
+
return content;
|
|
81
|
+
failures.push('coder-canonical:empty response');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
failures.push(`coder-canonical:${safeFailure(error?.response?.data?.error || error?.message)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (this.dependencies.hasSelfHostedTransport() && this.dependencies.shouldTrySelfHosted(resolvedModel, model || resolvedModel)) {
|
|
88
|
+
try {
|
|
89
|
+
const content = extract(await this.dependencies.postSelfHosted(body));
|
|
90
|
+
if (content)
|
|
91
|
+
return content;
|
|
92
|
+
failures.push('self-hosted:empty response');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
failures.push(`self-hosted:${safeFailure(error?.message)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.lastErrors = failures;
|
|
99
|
+
throw new Error(`No permitted code-completion backend returned a response${failures.length ? ` (${failures.join(' | ')})` : ''}.`);
|
|
40
100
|
}
|
|
41
101
|
response(data, model, requested, prefix) {
|
|
42
102
|
const content = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || data?.response || data?.message || data?.content;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AxiosInstance } from 'axios';
|
|
2
2
|
export { redactSensitiveText } from './secret-policy.js';
|
|
3
|
-
export type EndpointAudience = 'auto' | 'coder' | 'models' | 'hub' | 'community' | 'music' | 'brain-hub' | 'mcp' | 'v3' | 'template' | 'hyperloop' | 'vigflow' | 'bridge' | 'release' | 'public';
|
|
3
|
+
export type EndpointAudience = 'auto' | 'coder' | 'models' | 'operator' | 'hub' | 'community' | 'music' | 'brain-hub' | 'mcp' | 'v3' | 'template' | 'hyperloop' | 'vigflow' | 'bridge' | 'release' | 'public';
|
|
4
4
|
export declare const DEFAULT_NETWORK_TIMEOUT_MS = 30000;
|
|
5
5
|
export declare class NetworkPolicyError extends Error {
|
|
6
6
|
readonly code: 'offline' | 'untrusted_endpoint' | 'credential_audience' | 'unsafe_redirect';
|
|
@@ -3,7 +3,8 @@ export { redactSensitiveText } from './secret-policy.js';
|
|
|
3
3
|
const AUDIENCE_HOSTS = {
|
|
4
4
|
coder: new Set(['coder.vigthoria.io']),
|
|
5
5
|
models: new Set(['api.vigthoria.io']),
|
|
6
|
-
|
|
6
|
+
operator: new Set(['api.vigthoria.io', 'operator.vigthoria.io']),
|
|
7
|
+
hub: new Set(['hub.vigthoria.io']),
|
|
7
8
|
community: new Set(['community.vigthoria.io']),
|
|
8
9
|
music: new Set(['music.vigthoria.io']),
|
|
9
10
|
'brain-hub': new Set(['coder.vigthoria.io']),
|
|
@@ -11,7 +12,7 @@ const AUDIENCE_HOSTS = {
|
|
|
11
12
|
v3: new Set(['coder.vigthoria.io']),
|
|
12
13
|
template: new Set(['coder.vigthoria.io', 'template.vigthoria.io']),
|
|
13
14
|
hyperloop: new Set(['coder.vigthoria.io', 'hyperloop.vigthoria.io']),
|
|
14
|
-
vigflow: new Set(['coder.vigthoria.io', '
|
|
15
|
+
vigflow: new Set(['coder.vigthoria.io', 'workflow.vigthoria.io']),
|
|
15
16
|
bridge: new Set(['bridge.vigthoria.io', 'devtools.vigthoria.io']),
|
|
16
17
|
release: new Set([
|
|
17
18
|
'extension.vigthoria.io',
|
|
@@ -20,12 +20,12 @@ export class OptionalPuppeteerScreenshotAdapter {
|
|
|
20
20
|
const loaded = await this.loadPuppeteer().catch(() => null);
|
|
21
21
|
const puppeteer = (loaded?.default || loaded);
|
|
22
22
|
if (!puppeteer || typeof puppeteer.launch !== 'function') {
|
|
23
|
-
return { captured: false, error: '
|
|
23
|
+
return { captured: false, error: 'required Puppeteer screenshot runtime is not installed' };
|
|
24
24
|
}
|
|
25
25
|
const browserProfile = this.allocateTemp('browser-');
|
|
26
26
|
try {
|
|
27
27
|
const browser = await puppeteer.launch({
|
|
28
|
-
headless:
|
|
28
|
+
headless: 'shell',
|
|
29
29
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
30
30
|
userDataDir: browserProfile,
|
|
31
31
|
});
|
|
@@ -104,7 +104,10 @@ export function installConsoleRedaction() {
|
|
|
104
104
|
}
|
|
105
105
|
export function scanOutboundContext(input) {
|
|
106
106
|
const findings = new Set();
|
|
107
|
-
|
|
107
|
+
// Track only the active recursion path. A WeakSet of every previously seen
|
|
108
|
+
// object incorrectly rejects ordinary DAGs where compatibility aliases point
|
|
109
|
+
// at the same context object. Only an ancestor reference is a real cycle.
|
|
110
|
+
const ancestors = new WeakSet();
|
|
108
111
|
const visit = (value, location, depth) => {
|
|
109
112
|
if (depth > 24)
|
|
110
113
|
throw new OutboundContextError([`${location}: depth limit exceeded`]);
|
|
@@ -119,26 +122,32 @@ export function scanOutboundContext(input) {
|
|
|
119
122
|
}
|
|
120
123
|
if (!value || typeof value !== 'object')
|
|
121
124
|
return value;
|
|
122
|
-
|
|
125
|
+
const objectValue = value;
|
|
126
|
+
if (ancestors.has(objectValue))
|
|
123
127
|
throw new OutboundContextError([`${location}: cyclic value`]);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
ancestors.add(objectValue);
|
|
129
|
+
try {
|
|
130
|
+
if (Array.isArray(value))
|
|
131
|
+
return value.map((entry, index) => visit(entry, `${location}[${index}]`, depth + 1));
|
|
132
|
+
const output = {};
|
|
133
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
134
|
+
const child = `${location}.${key}`;
|
|
135
|
+
if (isSensitivePath(key)) {
|
|
136
|
+
findings.add(`${child}: sensitive path excluded`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (SENSITIVE_KEY.test(key)) {
|
|
140
|
+
findings.add(`${child}: credential field redacted`);
|
|
141
|
+
output[key] = REDACTED;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
output[key] = visit(entry, child, depth + 1);
|
|
133
145
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
output[key] = visit(entry, child, depth + 1);
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
ancestors.delete(objectValue);
|
|
140
150
|
}
|
|
141
|
-
return output;
|
|
142
151
|
};
|
|
143
152
|
const value = visit(input, '$', 0);
|
|
144
153
|
if (containsHighConfidenceSecret(JSON.stringify(value))) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface NormalizedSubscription {
|
|
2
|
+
plan: string;
|
|
3
|
+
status: string;
|
|
4
|
+
expiresAt?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Normalize the subscription shapes emitted by the Coder, User Hub, and
|
|
8
|
+
* legacy authentication contracts. Missing plan data is never synthesized:
|
|
9
|
+
* callers must retain their prior authoritative value or fail closed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeSubscriptionResponse(payload: unknown): NormalizedSubscription | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: {};
|
|
5
|
+
}
|
|
6
|
+
function firstString(...values) {
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
if (typeof value === 'string' && value.trim())
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Normalize the subscription shapes emitted by the Coder, User Hub, and
|
|
15
|
+
* legacy authentication contracts. Missing plan data is never synthesized:
|
|
16
|
+
* callers must retain their prior authoritative value or fail closed.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeSubscriptionResponse(payload) {
|
|
19
|
+
const body = record(payload);
|
|
20
|
+
const subscription = record(body.subscription);
|
|
21
|
+
const user = record(body.user);
|
|
22
|
+
const userSubscription = record(user.subscription);
|
|
23
|
+
const plan = firstString(body.plan, body.subscription_plan, body.subscriptionPlan, body.subscriptionTier, body.tier, subscription.plan, subscription.name, subscription.subscription_plan, subscription.subscriptionPlan, subscription.subscriptionTier, subscription.tier, user.plan, user.subscription_plan, user.subscriptionPlan, user.subscriptionTier, user.tier, userSubscription.plan, userSubscription.name, userSubscription.subscription_plan, userSubscription.subscriptionPlan, userSubscription.subscriptionTier, userSubscription.tier);
|
|
24
|
+
if (!plan)
|
|
25
|
+
return null;
|
|
26
|
+
const explicitStatus = firstString(body.status, body.subscription_status, body.subscriptionStatus, subscription.status, subscription.subscription_status, subscription.subscriptionStatus, user.status, user.subscription_status, user.subscriptionStatus, userSubscription.status, userSubscription.subscription_status, userSubscription.subscriptionStatus);
|
|
27
|
+
const activeFlag = [body.active, subscription.active, user.active, userSubscription.active]
|
|
28
|
+
.find((value) => typeof value === 'boolean');
|
|
29
|
+
const status = explicitStatus || (activeFlag === false ? 'inactive' : 'active');
|
|
30
|
+
const expiresAt = firstString(body.expiresAt, body.expires_at, subscription.expiresAt, subscription.expires_at, user.expiresAt, user.expires_at, userSubscription.expiresAt, userSubscription.expires_at);
|
|
31
|
+
return { plan, status: status.toLowerCase(), ...(expiresAt ? { expiresAt } : {}) };
|
|
32
|
+
}
|
|
@@ -135,7 +135,11 @@ export class V3WorkspaceService {
|
|
|
135
135
|
const candidates = new Set();
|
|
136
136
|
for (const value of [message, context.rawMessage, context.agentPrompt]) {
|
|
137
137
|
const text = String(value || '');
|
|
138
|
-
|
|
138
|
+
const extensions = 'c|cc|cpp|cxx|h|hpp|cs|css|go|html|htm|ini|java|js|jsx|json|kt|kts|md|mjs|cjs|php|ps1|py|rb|rs|scss|sh|sql|svelte|toml|ts|tsx|txt|vue|xml|yaml|yml';
|
|
139
|
+
for (const pattern of [
|
|
140
|
+
new RegExp('`([^`]+\\.(?:' + extensions + '))`', 'gi'),
|
|
141
|
+
new RegExp('\\b([A-Za-z0-9_./-]+\\.(?:' + extensions + '))\\b', 'gi'),
|
|
142
|
+
]) {
|
|
139
143
|
let match;
|
|
140
144
|
while ((match = pattern.exec(text)) !== null) {
|
|
141
145
|
const filePath = match[1].trim().replace(/^\.\//, '');
|
|
@@ -16,8 +16,8 @@ export class VigFlowClient {
|
|
|
16
16
|
const authToken = this.dependencies.getAccessToken();
|
|
17
17
|
if (!authToken)
|
|
18
18
|
throw new Error('Not authenticated. Run vigthoria login first.');
|
|
19
|
-
const response = await this.transport.post(`${baseUrl}/api/auth/sso`, {
|
|
20
|
-
headers: { 'Content-Type': 'application/json' }, timeout: 30_000,
|
|
19
|
+
const response = await this.transport.post(`${baseUrl}/api/auth/sso`, {}, {
|
|
20
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}` }, timeout: 30_000,
|
|
21
21
|
});
|
|
22
22
|
const token = String(response.data.token || '').trim();
|
|
23
23
|
if (!token)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigthoria-cli",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.25",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js",
|
|
94
94
|
"test:game:command": "node scripts/test-game-command.mjs",
|
|
95
95
|
"test:security:dependencies": "node scripts/test-dependency-security.mjs",
|
|
96
|
-
"test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs",
|
|
96
|
+
"test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs && node scripts/test-local-security-service.mjs",
|
|
97
97
|
"test:security:workspace": "npm run build && node scripts/test-workspace-archive-containment.mjs",
|
|
98
98
|
"test:security:process": "npm run build && node scripts/test-process-approval-safety.mjs",
|
|
99
99
|
"test:security:mutation-preview": "npm run build && node scripts/test-mutation-preview-transaction.mjs",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"marked": "^11.0.0",
|
|
132
132
|
"marked-terminal": "^6.2.0",
|
|
133
133
|
"ora": "^7.0.1",
|
|
134
|
+
"puppeteer": "^24.40.0",
|
|
134
135
|
"ws": "^8.14.2"
|
|
135
136
|
},
|
|
136
137
|
"devDependencies": {
|
|
@@ -138,18 +139,9 @@
|
|
|
138
139
|
"@types/inquirer": "^9.0.7",
|
|
139
140
|
"@types/node": "^20.10.0",
|
|
140
141
|
"@types/ws": "^8.5.10",
|
|
141
|
-
"puppeteer": "^24.40.0",
|
|
142
142
|
"ts-node": "^10.9.2",
|
|
143
143
|
"typescript": "^5.3.2"
|
|
144
144
|
},
|
|
145
|
-
"peerDependencies": {
|
|
146
|
-
"puppeteer": "^24.40.0"
|
|
147
|
-
},
|
|
148
|
-
"peerDependenciesMeta": {
|
|
149
|
-
"puppeteer": {
|
|
150
|
-
"optional": true
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
145
|
"engines": {
|
|
154
146
|
"node": ">=20.19.0"
|
|
155
147
|
},
|
package/release-policy.json
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"signature": {
|
|
28
28
|
"algorithm": "Ed25519",
|
|
29
29
|
"trustedKeys": {
|
|
30
|
-
"vigthoria-release-2026-01": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAotTXdcHwZ+D1z5lJMdgAAeMU99BRdMCaCZ465DN2aug=\n-----END PUBLIC KEY-----\n"
|
|
30
|
+
"vigthoria-release-2026-01": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAotTXdcHwZ+D1z5lJMdgAAeMU99BRdMCaCZ465DN2aug=\n-----END PUBLIC KEY-----\n",
|
|
31
|
+
"vigthoria-release-2026-02": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAwzjKsJCJhF387ZPbQoGTciJtOLyJFXp4zpOKrrhQm8I=\n-----END PUBLIC KEY-----\n"
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
"channels": {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const archive = path.resolve(String(process.argv[2] || ''));
|
|
8
|
+
const outputDirectory = path.resolve(String(process.argv[3] || ''));
|
|
9
|
+
if (!fs.statSync(archive, { throwIfNoEntry: false })?.isFile()) throw new Error('Usage: generate-release-evidence.mjs <archive.tgz> <output-directory>');
|
|
10
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
11
|
+
const sha256 = createHash('sha256').update(fs.readFileSync(archive)).digest('hex');
|
|
12
|
+
const readPackage = spawnSync('tar', ['-xOf', archive, 'package/package.json'], { encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
13
|
+
if (readPackage.status !== 0) throw new Error(`Unable to inspect archive: ${readPackage.stderr || readPackage.stdout}`);
|
|
14
|
+
const pkg = JSON.parse(readPackage.stdout);
|
|
15
|
+
if (pkg.name !== 'vigthoria-cli' || !pkg.version) throw new Error('Archive package identity is invalid');
|
|
16
|
+
const timestamp = new Date().toISOString();
|
|
17
|
+
const component = {
|
|
18
|
+
type: 'application',
|
|
19
|
+
'bom-ref': `pkg:npm/${pkg.name}@${pkg.version}`,
|
|
20
|
+
name: pkg.name,
|
|
21
|
+
version: pkg.version,
|
|
22
|
+
purl: `pkg:npm/${pkg.name}@${pkg.version}`,
|
|
23
|
+
hashes: [{ alg: 'SHA-256', content: sha256 }],
|
|
24
|
+
};
|
|
25
|
+
const components = Object.entries(pkg.dependencies || {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, version]) => ({
|
|
26
|
+
type: 'library', name, version: String(version), purl: `pkg:npm/${encodeURIComponent(name)}@${encodeURIComponent(String(version))}`,
|
|
27
|
+
scope: 'required',
|
|
28
|
+
}));
|
|
29
|
+
const sbom = {
|
|
30
|
+
bomFormat: 'CycloneDX', specVersion: '1.5', serialNumber: `urn:uuid:${randomUUID()}`, version: 1,
|
|
31
|
+
metadata: { timestamp, tools: { components: [{ type: 'application', name: 'vigthoria-cli-release-evidence', version: '1' }] }, component },
|
|
32
|
+
components,
|
|
33
|
+
};
|
|
34
|
+
const provenance = {
|
|
35
|
+
_type: 'https://in-toto.io/Statement/v1',
|
|
36
|
+
subject: [{ name: path.basename(archive), digest: { sha256 } }],
|
|
37
|
+
predicateType: 'https://vigthoria.io/provenance/verified-existing-artifact/v1',
|
|
38
|
+
predicate: {
|
|
39
|
+
package: { name: pkg.name, version: pkg.version },
|
|
40
|
+
verification: { method: 'sha256-and-embedded-package-identity', verifiedAt: timestamp },
|
|
41
|
+
claimBoundary: 'This record verifies an existing immutable artifact; it does not reconstruct or attest to its original build environment.',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const stem = `${pkg.name}-${pkg.version}.tgz`;
|
|
45
|
+
const sbomPath = path.join(outputDirectory, `${stem}.sbom.json`);
|
|
46
|
+
const provenancePath = path.join(outputDirectory, `${stem}.provenance.json`);
|
|
47
|
+
fs.writeFileSync(sbomPath, `${JSON.stringify(sbom, null, 2)}\n`, { mode: 0o644 });
|
|
48
|
+
fs.writeFileSync(provenancePath, `${JSON.stringify(provenance, null, 2)}\n`, { mode: 0o644 });
|
|
49
|
+
console.log(JSON.stringify({ packageName: pkg.name, version: pkg.version, sha256, size: fs.statSync(archive).size, sbomPath, provenancePath }, null, 2));
|
|
@@ -20,6 +20,9 @@ const privateKeyFile = String(process.env.RELEASE_SIGNING_PRIVATE_KEY_FILE || ''
|
|
|
20
20
|
const keyId = String(process.env.RELEASE_SIGNING_KEY_ID || '');
|
|
21
21
|
const sbomFile = String(process.env.RELEASE_SBOM_FILE || '');
|
|
22
22
|
const provenanceFile = String(process.env.RELEASE_PROVENANCE_FILE || '');
|
|
23
|
+
const externalArchive = String(process.env.RELEASE_ARCHIVE_FILE || '');
|
|
24
|
+
const republishExisting = process.env.RELEASE_REPUBLISH_EXISTING === '1';
|
|
25
|
+
const resetManifest = process.env.RELEASE_RESET_MANIFEST === '1';
|
|
23
26
|
const policy = getReleasePolicy();
|
|
24
27
|
|
|
25
28
|
function stop(message) { throw new Error(message); }
|
|
@@ -41,14 +44,17 @@ requiredFile(sbomFile, 'RELEASE_SBOM_FILE');
|
|
|
41
44
|
requiredFile(provenanceFile, 'RELEASE_PROVENANCE_FILE');
|
|
42
45
|
if (!Object.hasOwn(policy.signature.trustedKeys, keyId)) stop(`RELEASE_SIGNING_KEY_ID is not trusted by release-policy.json: ${keyId || '(unset)'}`);
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
47
|
+
if (externalArchive && !republishExisting) stop('RELEASE_ARCHIVE_FILE requires RELEASE_REPUBLISH_EXISTING=1.');
|
|
48
|
+
if (!externalArchive) {
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
50
|
+
const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
|
|
51
|
+
if ([pkg.version, lock.version, lock.packages?.['']?.version].some((identity) => identity !== version)) stop('Requested version conflicts with package.json or package-lock.json.');
|
|
52
|
+
const runtime = spawnSync(process.execPath, ['dist/index.js', '--version'], { cwd: root, encoding: 'utf8', env: { ...process.env, VIGTHORIA_OFFLINE: '1', VIGTHORIA_NO_BANNER: '1' } });
|
|
53
|
+
if (runtime.status !== 0 || runtime.stdout.trim() !== version) stop('Requested version conflicts with the built CLI banner.');
|
|
54
|
+
}
|
|
49
55
|
|
|
50
56
|
const archiveFilename = `${policy.packageName}-${version}.tgz`;
|
|
51
|
-
const archive = path.join(root, archiveFilename);
|
|
57
|
+
const archive = externalArchive ? path.resolve(externalArchive) : path.join(root, archiveFilename);
|
|
52
58
|
requiredFile(archive, 'Release archive');
|
|
53
59
|
const metadata = exactPackageMetadata(archive);
|
|
54
60
|
if (metadata.name !== policy.packageName || metadata.version !== version) stop('Requested version conflicts with archive metadata.');
|
|
@@ -87,16 +93,21 @@ try {
|
|
|
87
93
|
verifyReleaseSignature(channel, entry, detached);
|
|
88
94
|
|
|
89
95
|
let manifest = { schemaVersion: policy.manifest.schemaVersion, channels: {} };
|
|
90
|
-
if (fs.existsSync(manifestFile)) manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
96
|
+
if (!resetManifest && fs.existsSync(manifestFile)) manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
91
97
|
manifest.channels = { ...manifest.channels, [channel]: entry };
|
|
92
98
|
parseReleaseManifest(manifest, channel);
|
|
93
99
|
|
|
94
100
|
const inputs = [[archive, archiveFilename], [sbomFile, sbomFilename], [provenanceFile, provenanceFilename]];
|
|
95
101
|
const staged = [];
|
|
96
102
|
for (const [source, filename] of inputs) {
|
|
103
|
+
const destination = path.join(downloadsDirectory, filename);
|
|
104
|
+
if (fs.existsSync(destination)) {
|
|
105
|
+
if (republishExisting && digest(destination) === digest(source) && fs.statSync(destination).size === fs.statSync(source).size) continue;
|
|
106
|
+
stop(`Refusing to overwrite published release component: ${destination}`);
|
|
107
|
+
}
|
|
97
108
|
const temporary = path.join(downloadsDirectory, `.${filename}.${suffix}.tmp`);
|
|
98
109
|
fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
|
|
99
|
-
staged.push([temporary,
|
|
110
|
+
staged.push([temporary, destination]);
|
|
100
111
|
}
|
|
101
112
|
const signatureTemporary = path.join(downloadsDirectory, `.${signatureFilename}.${suffix}.tmp`);
|
|
102
113
|
fs.writeFileSync(signatureTemporary, `${detached}\n`, { flag: 'wx', mode: 0o644 });
|