knoxis-helper 1.4.4 → 1.4.5
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/lib/knoxis-local-agent.js +180 -28
- package/package.json +1 -1
|
@@ -28,6 +28,8 @@ const DEFAULT_PORT = parseInt(process.env.KNOXIS_AGENT_PORT || '3456', 10);
|
|
|
28
28
|
const CERT_DIR = process.env.KNOXIS_CERT_DIR || path.join(os.homedir(), '.knoxis', 'certs');
|
|
29
29
|
const CERT_FILE = process.env.KNOXIS_CERT_FILE || path.join(CERT_DIR, 'localhost.pem');
|
|
30
30
|
const KEY_FILE = process.env.KNOXIS_CERT_KEY || path.join(CERT_DIR, 'localhost-key.pem');
|
|
31
|
+
const PFX_FILE = process.env.KNOXIS_CERT_PFX || path.join(CERT_DIR, 'localhost.pfx');
|
|
32
|
+
const PFX_PASS = 'knoxis-local'; // Not a real secret — self-signed cert for localhost only
|
|
31
33
|
|
|
32
34
|
// Trusted origins for CORS (deployed frontends)
|
|
33
35
|
const TRUSTED_ORIGINS = [
|
|
@@ -77,10 +79,116 @@ function generateSelfSignedCert() {
|
|
|
77
79
|
return true;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
console.warn('⚠️ OpenSSL
|
|
82
|
+
console.warn('⚠️ OpenSSL not available');
|
|
81
83
|
return false;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Windows fallback: generate a self-signed certificate via PowerShell.
|
|
88
|
+
* Uses New-SelfSignedCertificate (built into Windows 10+ / Server 2016+).
|
|
89
|
+
* Always produces a PFX file. Also attempts PEM export when .NET 5+ is present.
|
|
90
|
+
* Cert is created in the user's personal store (no admin required) and removed
|
|
91
|
+
* from the store after export.
|
|
92
|
+
*/
|
|
93
|
+
function generateSelfSignedCertWindows() {
|
|
94
|
+
console.log('🔐 Generating self-signed certificate via PowerShell...');
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(CERT_DIR)) {
|
|
97
|
+
fs.mkdirSync(CERT_DIR, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Write a PS1 script to a temp file to avoid all shell-escaping issues.
|
|
101
|
+
const psLines = [
|
|
102
|
+
'$ErrorActionPreference = "Stop"',
|
|
103
|
+
'$cert = New-SelfSignedCertificate -Subject "CN=localhost" -DnsName "localhost","127.0.0.1" `',
|
|
104
|
+
' -CertStoreLocation "Cert:\\CurrentUser\\My" -NotAfter (Get-Date).AddDays(365) `',
|
|
105
|
+
' -KeyExportPolicy Exportable -KeySpec KeyExchange',
|
|
106
|
+
'$thumb = $cert.Thumbprint',
|
|
107
|
+
'',
|
|
108
|
+
'# Export PFX (works on all Windows versions with this cmdlet)',
|
|
109
|
+
'$pwd = ConvertTo-SecureString -String "' + PFX_PASS + '" -Force -AsPlainText',
|
|
110
|
+
'Export-PfxCertificate -Cert "Cert:\\CurrentUser\\My\\$thumb" -FilePath "' + PFX_FILE + '" -Password $pwd | Out-Null',
|
|
111
|
+
'',
|
|
112
|
+
'# Attempt PEM export — only works on .NET 5+ (PowerShell 7+)',
|
|
113
|
+
'try {',
|
|
114
|
+
' $certDer = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)',
|
|
115
|
+
' $certB64 = [Convert]::ToBase64String($certDer, "InsertLineBreaks")',
|
|
116
|
+
' $certPem = "-----BEGIN CERTIFICATE-----`r`n$certB64`r`n-----END CERTIFICATE-----"',
|
|
117
|
+
' [System.IO.File]::WriteAllText("' + CERT_FILE + '", $certPem)',
|
|
118
|
+
'',
|
|
119
|
+
' $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)',
|
|
120
|
+
' $keyBytes = $rsa.ExportPkcs8PrivateKey()',
|
|
121
|
+
' $keyB64 = [Convert]::ToBase64String($keyBytes, "InsertLineBreaks")',
|
|
122
|
+
' $keyPem = "-----BEGIN PRIVATE KEY-----`r`n$keyB64`r`n-----END PRIVATE KEY-----"',
|
|
123
|
+
' [System.IO.File]::WriteAllText("' + KEY_FILE + '", $keyPem)',
|
|
124
|
+
'} catch {',
|
|
125
|
+
' # PEM export not available on this .NET version — PFX is sufficient',
|
|
126
|
+
'}',
|
|
127
|
+
'',
|
|
128
|
+
'# Remove cert from user store (the exported files are all we need)',
|
|
129
|
+
'Remove-Item "Cert:\\CurrentUser\\My\\$thumb" -Force -ErrorAction SilentlyContinue',
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const tmpScript = path.join(os.tmpdir(), 'knoxis-cert-' + Date.now() + '.ps1');
|
|
133
|
+
fs.writeFileSync(tmpScript, psLines.join('\r\n'), 'utf8');
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = spawnSync('powershell', [
|
|
137
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
138
|
+
'-File', tmpScript
|
|
139
|
+
], { stdio: 'pipe', timeout: 30000 });
|
|
140
|
+
|
|
141
|
+
if (result.status === 0 && fs.existsSync(PFX_FILE)) {
|
|
142
|
+
console.log('✅ Certificate generated via PowerShell');
|
|
143
|
+
if (fs.existsSync(CERT_FILE) && fs.existsSync(KEY_FILE)) {
|
|
144
|
+
console.log(' PEM files exported (PFX + PEM)');
|
|
145
|
+
} else {
|
|
146
|
+
console.log(' PFX exported (PEM not available on this .NET version)');
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.warn('⚠️ PowerShell certificate generation failed');
|
|
152
|
+
if (result.stderr) {
|
|
153
|
+
const firstLine = result.stderr.toString().trim().split('\n')[0];
|
|
154
|
+
if (firstLine) console.warn(' ' + firstLine);
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
} finally {
|
|
158
|
+
try { fs.unlinkSync(tmpScript); } catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Try to create an HTTPS server from the available cert material.
|
|
164
|
+
* Returns { opts, source } on success, null on failure.
|
|
165
|
+
* Checks PEM first (cross-platform), then PFX (Windows PowerShell fallback).
|
|
166
|
+
*/
|
|
167
|
+
function tryLoadCerts() {
|
|
168
|
+
// Prefer PEM files (generated by openssl or PS .NET 5+)
|
|
169
|
+
try {
|
|
170
|
+
if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
|
|
171
|
+
const key = fs.readFileSync(KEY_FILE);
|
|
172
|
+
const cert = fs.readFileSync(CERT_FILE);
|
|
173
|
+
return { opts: { key, cert }, source: 'PEM' };
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.warn('⚠️ Failed to load PEM certificates:', err.message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fallback: PFX file (Windows PS 5.1 where PEM export isn't available)
|
|
180
|
+
try {
|
|
181
|
+
if (fs.existsSync(PFX_FILE)) {
|
|
182
|
+
const pfx = fs.readFileSync(PFX_FILE);
|
|
183
|
+
return { opts: { pfx, passphrase: PFX_PASS }, source: 'PFX' };
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.warn('⚠️ Failed to load PFX certificate:', err.message);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
84
192
|
/**
|
|
85
193
|
* Get CORS headers for the given request origin
|
|
86
194
|
*/
|
|
@@ -132,6 +240,38 @@ function buildShellCommand(command) {
|
|
|
132
240
|
return { cmd: 'bash', args: ['-lc', command] };
|
|
133
241
|
}
|
|
134
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Build a cross-platform command to pipe a file's contents to stdout.
|
|
245
|
+
* Uses `type` on Windows (cmd.exe) and `cat` on Unix.
|
|
246
|
+
*/
|
|
247
|
+
function buildCatCommand(filePath) {
|
|
248
|
+
if (os.platform() === 'win32') {
|
|
249
|
+
return `type "${filePath}"`;
|
|
250
|
+
}
|
|
251
|
+
return `cat "${filePath}"`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build a cross-platform command string that sets environment variables
|
|
256
|
+
* before running a child command.
|
|
257
|
+
* Unix: VAR="val" command
|
|
258
|
+
* Windows: set "VAR=val" && command
|
|
259
|
+
* @param {Record<string,string>} envVars
|
|
260
|
+
* @param {string} command
|
|
261
|
+
*/
|
|
262
|
+
function buildEnvCommand(envVars, command) {
|
|
263
|
+
if (os.platform() === 'win32') {
|
|
264
|
+
const sets = Object.entries(envVars)
|
|
265
|
+
.map(([k, v]) => `set "${k}=${v}"`)
|
|
266
|
+
.join(' && ');
|
|
267
|
+
return `${sets} && ${command}`;
|
|
268
|
+
}
|
|
269
|
+
const prefix = Object.entries(envVars)
|
|
270
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
271
|
+
.join(' ');
|
|
272
|
+
return `${prefix} ${command}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
135
275
|
function commandExists(cmd) {
|
|
136
276
|
const detector = os.platform() === 'win32' ? 'where' : 'which';
|
|
137
277
|
const result = spawnSync(detector, [cmd], { stdio: 'ignore' });
|
|
@@ -509,7 +649,7 @@ async function handleRequest(req, res) {
|
|
|
509
649
|
if (prompt && prompt.trim().length > 0) {
|
|
510
650
|
promptFile = path.join(os.tmpdir(), 'knoxis-task-' + (sessionId || Date.now()) + '.txt');
|
|
511
651
|
fs.writeFileSync(promptFile, prompt, 'utf8');
|
|
512
|
-
finalCommand =
|
|
652
|
+
finalCommand = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
513
653
|
console.log('📝 Task written to ' + promptFile + ' (' + prompt.length + ' chars)');
|
|
514
654
|
}
|
|
515
655
|
|
|
@@ -768,17 +908,17 @@ async function handleRequest(req, res) {
|
|
|
768
908
|
const scriptPath = resolveInteractiveScript();
|
|
769
909
|
if (scriptPath) {
|
|
770
910
|
// Interactive mode: multi-turn with Groq pair programmer
|
|
771
|
-
command =
|
|
911
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
772
912
|
mode = 'interactive';
|
|
773
913
|
console.log(`🤝 Interactive mode: ${scriptPath}`);
|
|
774
914
|
} else {
|
|
775
915
|
// Interactive requested but script not found - fall back to single-shot
|
|
776
916
|
console.warn('⚠️ Interactive script not found, falling back to single-shot');
|
|
777
|
-
command =
|
|
917
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
778
918
|
}
|
|
779
919
|
} else {
|
|
780
920
|
// Standard single-shot mode: pipe task to Claude
|
|
781
|
-
command =
|
|
921
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
782
922
|
}
|
|
783
923
|
|
|
784
924
|
if (headless) {
|
|
@@ -979,34 +1119,38 @@ function openLinuxTerminal(workspaceDir, command) {
|
|
|
979
1119
|
}
|
|
980
1120
|
|
|
981
1121
|
function createServer() {
|
|
982
|
-
// Try to load existing certs
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const cert = fs.readFileSync(CERT_FILE);
|
|
1122
|
+
// 1. Try to load existing certs (PEM or PFX)
|
|
1123
|
+
const existing = tryLoadCerts();
|
|
1124
|
+
if (existing) {
|
|
1125
|
+
try {
|
|
987
1126
|
serverMeta.secure = true;
|
|
988
|
-
|
|
1127
|
+
console.log('🔒 Loaded existing ' + existing.source + ' certificate');
|
|
1128
|
+
return https.createServer(existing.opts, handleRequest);
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
console.warn('⚠️ Failed to create HTTPS server from existing certs:', err.message);
|
|
989
1131
|
}
|
|
990
|
-
} catch (err) {
|
|
991
|
-
console.warn('⚠️ Failed to load TLS certificates:', err.message);
|
|
992
1132
|
}
|
|
993
1133
|
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1134
|
+
// 2. Generate new certs — try openssl first, then Windows PowerShell
|
|
1135
|
+
let generated = generateSelfSignedCert();
|
|
1136
|
+
if (!generated && os.platform() === 'win32') {
|
|
1137
|
+
generated = generateSelfSignedCertWindows();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (generated) {
|
|
1141
|
+
const fresh = tryLoadCerts();
|
|
1142
|
+
if (fresh) {
|
|
998
1143
|
try {
|
|
999
|
-
const key = fs.readFileSync(KEY_FILE);
|
|
1000
|
-
const cert = fs.readFileSync(CERT_FILE);
|
|
1001
1144
|
serverMeta.secure = true;
|
|
1002
|
-
|
|
1145
|
+
console.log('🔒 Using freshly generated ' + fresh.source + ' certificate');
|
|
1146
|
+
return https.createServer(fresh.opts, handleRequest);
|
|
1003
1147
|
} catch (err) {
|
|
1004
|
-
console.warn('⚠️ Failed to
|
|
1148
|
+
console.warn('⚠️ Failed to create HTTPS server from generated certs:', err.message);
|
|
1005
1149
|
}
|
|
1006
1150
|
}
|
|
1007
1151
|
}
|
|
1008
1152
|
|
|
1009
|
-
// Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
|
|
1153
|
+
// 3. Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
|
|
1010
1154
|
serverMeta.secure = false;
|
|
1011
1155
|
console.warn('');
|
|
1012
1156
|
console.warn('⚠️ RUNNING IN HTTP MODE - This will cause 405/CORS errors from HTTPS frontends!');
|
|
@@ -1203,14 +1347,14 @@ function connectRelayWebSocket() {
|
|
|
1203
1347
|
// Interactive mode: use multi-turn pair programming script
|
|
1204
1348
|
const scriptPath = resolveInteractiveScript();
|
|
1205
1349
|
if (scriptPath) {
|
|
1206
|
-
command =
|
|
1350
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
1207
1351
|
console.log(` 🤝 Interactive mode: ${scriptPath}`);
|
|
1208
1352
|
} else {
|
|
1209
|
-
command =
|
|
1353
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1210
1354
|
console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
|
|
1211
1355
|
}
|
|
1212
1356
|
} else {
|
|
1213
|
-
command =
|
|
1357
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1214
1358
|
}
|
|
1215
1359
|
console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
|
|
1216
1360
|
} else {
|
|
@@ -1429,9 +1573,17 @@ server.listen(serverMeta.port, () => {
|
|
|
1429
1573
|
console.warn('║ Browsers will block requests from HTTPS sites (like qig.ai) ║');
|
|
1430
1574
|
console.warn('╚══════════════════════════════════════════════════════════════╝');
|
|
1431
1575
|
console.warn('');
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1576
|
+
if (os.platform() === 'win32') {
|
|
1577
|
+
console.warn('To fix this on Windows, try one of:');
|
|
1578
|
+
console.warn(' 1. winget install ShiningLight.OpenSSL.Light (then restart)');
|
|
1579
|
+
console.warn(' 2. choco install openssl (then restart)');
|
|
1580
|
+
console.warn(' 3. Install Git for Windows (ships with OpenSSL)');
|
|
1581
|
+
console.warn(` 4. Manually place PEM or PFX certs in: ${CERT_DIR}`);
|
|
1582
|
+
} else {
|
|
1583
|
+
console.warn('To fix this, either:');
|
|
1584
|
+
console.warn(' 1. Install OpenSSL and restart (auto-generates certs)');
|
|
1585
|
+
console.warn(` 2. Manually place certs in: ${CERT_DIR}`);
|
|
1586
|
+
}
|
|
1435
1587
|
console.warn('');
|
|
1436
1588
|
} else {
|
|
1437
1589
|
console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
|