knoxis-helper 1.4.3 → 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 +238 -37
- 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) {
|
|
@@ -876,7 +1016,15 @@ end tell
|
|
|
876
1016
|
}
|
|
877
1017
|
|
|
878
1018
|
/**
|
|
879
|
-
* Open terminal on Windows
|
|
1019
|
+
* Open terminal on Windows.
|
|
1020
|
+
*
|
|
1021
|
+
* If the agent is running elevated (admin), Windows won't let us spawn an
|
|
1022
|
+
* unelevated child directly — `start cmd` inherits the parent's integrity
|
|
1023
|
+
* level. We detect that case and route through a one-shot scheduled task
|
|
1024
|
+
* at /rl LIMITED, which runs as the interactive user at medium integrity.
|
|
1025
|
+
* That matters because claude's credentials live under the user's real
|
|
1026
|
+
* %USERPROFILE%\.claude\, not the admin profile, so an elevated cmd can't
|
|
1027
|
+
* find them and hangs on launch.
|
|
880
1028
|
*/
|
|
881
1029
|
function openWindowsTerminal(workspaceDir, command) {
|
|
882
1030
|
return new Promise((resolve, reject) => {
|
|
@@ -884,16 +1032,57 @@ function openWindowsTerminal(workspaceDir, command) {
|
|
|
884
1032
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
885
1033
|
}
|
|
886
1034
|
|
|
887
|
-
|
|
1035
|
+
// `net session` exits 0 only when elevated.
|
|
1036
|
+
exec('net session >nul 2>&1', (elevErr) => {
|
|
1037
|
+
const isElevated = !elevErr;
|
|
888
1038
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1039
|
+
if (!isElevated) {
|
|
1040
|
+
const fullCommand = `start cmd /K "cd /d "${workspaceDir}" && ${command}"`;
|
|
1041
|
+
exec(fullCommand, (error) => {
|
|
1042
|
+
if (error) {
|
|
1043
|
+
console.error('❌ Windows terminal error:', error);
|
|
1044
|
+
reject(error);
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log('✅ Terminal opened on Windows');
|
|
1047
|
+
resolve();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
return;
|
|
896
1051
|
}
|
|
1052
|
+
|
|
1053
|
+
const tempBat = path.join(
|
|
1054
|
+
os.tmpdir(),
|
|
1055
|
+
`knoxis-launch-${process.pid}-${Date.now()}.bat`
|
|
1056
|
+
);
|
|
1057
|
+
const batContent = `@echo off\r\ncd /d "${workspaceDir}"\r\n${command}\r\ncmd /K\r\n`;
|
|
1058
|
+
fs.writeFileSync(tempBat, batContent);
|
|
1059
|
+
|
|
1060
|
+
const taskName = `KnoxisSpawn_${process.pid}_${Date.now()}`;
|
|
1061
|
+
const createCmd = `schtasks /create /tn "${taskName}" /tr "${tempBat}" /sc once /st 00:00 /it /rl LIMITED /f`;
|
|
1062
|
+
|
|
1063
|
+
exec(createCmd, (cErr) => {
|
|
1064
|
+
if (cErr) {
|
|
1065
|
+
try { fs.unlinkSync(tempBat); } catch (_) {}
|
|
1066
|
+
console.error('❌ Windows terminal error (schtasks create):', cErr);
|
|
1067
|
+
return reject(cErr);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
exec(`schtasks /run /tn "${taskName}"`, (rErr) => {
|
|
1071
|
+
// Task registration cleanup. Leave the .bat in %TEMP% — cmd may
|
|
1072
|
+
// still have an open handle to it, and the OS cleans %TEMP%.
|
|
1073
|
+
setTimeout(() => {
|
|
1074
|
+
exec(`schtasks /delete /tn "${taskName}" /f`, () => {});
|
|
1075
|
+
}, 5000);
|
|
1076
|
+
|
|
1077
|
+
if (rErr) {
|
|
1078
|
+
console.error('❌ Windows terminal error (schtasks run):', rErr);
|
|
1079
|
+
reject(rErr);
|
|
1080
|
+
} else {
|
|
1081
|
+
console.log('✅ Terminal opened on Windows (de-elevated via scheduled task)');
|
|
1082
|
+
resolve();
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
897
1086
|
});
|
|
898
1087
|
});
|
|
899
1088
|
}
|
|
@@ -930,34 +1119,38 @@ function openLinuxTerminal(workspaceDir, command) {
|
|
|
930
1119
|
}
|
|
931
1120
|
|
|
932
1121
|
function createServer() {
|
|
933
|
-
// Try to load existing certs
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
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 {
|
|
938
1126
|
serverMeta.secure = true;
|
|
939
|
-
|
|
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);
|
|
940
1131
|
}
|
|
941
|
-
} catch (err) {
|
|
942
|
-
console.warn('⚠️ Failed to load TLS certificates:', err.message);
|
|
943
1132
|
}
|
|
944
1133
|
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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) {
|
|
949
1143
|
try {
|
|
950
|
-
const key = fs.readFileSync(KEY_FILE);
|
|
951
|
-
const cert = fs.readFileSync(CERT_FILE);
|
|
952
1144
|
serverMeta.secure = true;
|
|
953
|
-
|
|
1145
|
+
console.log('🔒 Using freshly generated ' + fresh.source + ' certificate');
|
|
1146
|
+
return https.createServer(fresh.opts, handleRequest);
|
|
954
1147
|
} catch (err) {
|
|
955
|
-
console.warn('⚠️ Failed to
|
|
1148
|
+
console.warn('⚠️ Failed to create HTTPS server from generated certs:', err.message);
|
|
956
1149
|
}
|
|
957
1150
|
}
|
|
958
1151
|
}
|
|
959
1152
|
|
|
960
|
-
// Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
|
|
1153
|
+
// 3. Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
|
|
961
1154
|
serverMeta.secure = false;
|
|
962
1155
|
console.warn('');
|
|
963
1156
|
console.warn('⚠️ RUNNING IN HTTP MODE - This will cause 405/CORS errors from HTTPS frontends!');
|
|
@@ -1154,14 +1347,14 @@ function connectRelayWebSocket() {
|
|
|
1154
1347
|
// Interactive mode: use multi-turn pair programming script
|
|
1155
1348
|
const scriptPath = resolveInteractiveScript();
|
|
1156
1349
|
if (scriptPath) {
|
|
1157
|
-
command =
|
|
1350
|
+
command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
|
|
1158
1351
|
console.log(` 🤝 Interactive mode: ${scriptPath}`);
|
|
1159
1352
|
} else {
|
|
1160
|
-
command =
|
|
1353
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1161
1354
|
console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
|
|
1162
1355
|
}
|
|
1163
1356
|
} else {
|
|
1164
|
-
command =
|
|
1357
|
+
command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
|
|
1165
1358
|
}
|
|
1166
1359
|
console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
|
|
1167
1360
|
} else {
|
|
@@ -1380,9 +1573,17 @@ server.listen(serverMeta.port, () => {
|
|
|
1380
1573
|
console.warn('║ Browsers will block requests from HTTPS sites (like qig.ai) ║');
|
|
1381
1574
|
console.warn('╚══════════════════════════════════════════════════════════════╝');
|
|
1382
1575
|
console.warn('');
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
+
}
|
|
1386
1587
|
console.warn('');
|
|
1387
1588
|
} else {
|
|
1388
1589
|
console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
|