@whyvix/remote 0.3.3 → 0.3.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/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { RemoteExecutionManager } from "./remote-execution-manager.js";
|
|
|
8
8
|
import { DeploymentManager } from "./deployment-manager.js";
|
|
9
9
|
import { createToolExecutor } from "./tools.js";
|
|
10
10
|
|
|
11
|
-
export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUID(),deviceName=os.hostname(),token="",workspaces=[process.cwd()],remoteTargets=[],deploymentProfiles=[],agentVersion="unknown",capabilities=[],reconnectMs=1000,reconnect=true,audit=()=>{},log=console.log,errorLog=console.error,onRegistered=()=>{}}={}) {
|
|
11
|
+
export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUID(),deviceName=os.hostname(),token="",workspaces=[process.cwd()],remoteTargets=[],deploymentProfiles=[],agentVersion="unknown",capabilities=[],reconnectMs=1000,reconnect=true,heartbeatMs=30000,audit=()=>{},log=console.log,errorLog=console.error,onRegistered=()=>{}}={}) {
|
|
12
12
|
let ws=null,reconnectTimer=null,stopped=false,generation=0;
|
|
13
13
|
const security=createSecurity({token,workspaces,audit});
|
|
14
14
|
const processManager=new ProcessManager({audit});
|
|
@@ -25,10 +25,21 @@ export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUI
|
|
|
25
25
|
clearTimeout(reconnectTimer); reconnectTimer=null;
|
|
26
26
|
const current=++generation;
|
|
27
27
|
const socket=new WebSocket(gatewayUrl); ws=socket;
|
|
28
|
+
let alive=true,heartbeatTimer=null;
|
|
29
|
+
const stopHeartbeat=()=>{if(heartbeatTimer){clearInterval(heartbeatTimer);heartbeatTimer=null;}};
|
|
30
|
+
if(heartbeatMs>0){
|
|
31
|
+
socket.on("pong",()=>{alive=true;});
|
|
32
|
+
heartbeatTimer=setInterval(()=>{
|
|
33
|
+
if(ws!==socket||socket.readyState!==WebSocket.OPEN)return;
|
|
34
|
+
if(!alive){errorLog("[agent:heartbeat-timeout]","generation="+current);socket.terminate();return;}
|
|
35
|
+
alive=false;socket.ping();
|
|
36
|
+
},heartbeatMs);
|
|
37
|
+
heartbeatTimer.unref?.();
|
|
38
|
+
}
|
|
28
39
|
log("[agent:connecting]",gatewayUrl,"generation="+current);
|
|
29
40
|
socket.on("open",()=>{if(ws!==socket)return;log("[agent:connected]",gatewayUrl);socket.send(JSON.stringify(envelope(TYPES.REGISTER,{auth:{token},device:{id:deviceId,name:deviceName,platform:process.platform,agentVersion,capabilities:[...new Set(capabilities.map(String))].sort()}})));});
|
|
30
41
|
socket.on("message",async(raw)=>{if(ws!==socket)return;try{const m=parseMessage(raw);if(m.type===TYPES.REGISTERED){log("[agent:registered]",m.deviceId);onRegistered({deviceId:m.deviceId,authorizedAccount:m.authorizedAccount||""});return;}if(m.type!==TYPES.REQUEST)return;log("[tool:request]",m.tool,{path:m.arguments?.path,cwd:m.arguments?.cwd});try{const result=await executeTool(m.tool,m.arguments);if(m.tool==="device.ping")Object.assign(result,{deviceId,deviceName});log("[tool:success]",m.tool);if(socket.readyState===WebSocket.OPEN)socket.send(JSON.stringify(envelope(TYPES.RESPONSE,{requestId:m.requestId,result})));}catch(error){errorLog("[tool:rejected]",m.tool,error.code||"TOOL_ERROR",error.message);if(socket.readyState===WebSocket.OPEN)socket.send(JSON.stringify(envelope(TYPES.ERROR,{requestId:m.requestId,error:{code:error.code||"TOOL_ERROR",message:error.message}})));}}catch(error){errorLog("[agent:protocol-error]",error.message);}});
|
|
31
|
-
socket.on("close",(code)=>{log("[agent:disconnected]","generation="+current,"code="+code);if(ws===socket)ws=null;if(code===1008){errorLog("[agent:authorization-required] device credential rejected");return;}if(code===4001){errorLog("[agent:replaced] newer WHYVIX Remote connection is active");return;}scheduleReconnect("close");});
|
|
42
|
+
socket.on("close",(code)=>{stopHeartbeat();log("[agent:disconnected]","generation="+current,"code="+code);if(ws===socket)ws=null;if(code===1008){errorLog("[agent:authorization-required] device credential rejected");return;}if(code===4001){errorLog("[agent:replaced] newer WHYVIX Remote connection is active");return;}scheduleReconnect("close");});
|
|
32
43
|
socket.on("error",(e)=>{errorLog("[agent:socket-error]",e.message);scheduleReconnect("error");});
|
|
33
44
|
}
|
|
34
45
|
async function stop(){stopped=true;clearTimeout(reconnectTimer);reconnectTimer=null;const socket=ws;ws=null;if(socket&&socket.readyState<WebSocket.CLOSING)await new Promise(r=>{socket.once("close",r);socket.close();});}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_EXECUTABLES =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
php: "php",
|
|
9
|
-
composer: "composer"
|
|
10
|
-
});
|
|
4
|
+
const DEFAULT_EXECUTABLES = null;
|
|
5
|
+
const BLOCKED_SHELL_EXECUTABLES = new Set([
|
|
6
|
+
"cmd", "command", "powershell", "pwsh", "sh", "bash", "dash", "zsh", "fish", "wsl"
|
|
7
|
+
]);
|
|
11
8
|
|
|
12
9
|
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
13
10
|
|
|
@@ -20,19 +17,25 @@ function appendBounded(state, key, chunk, maxBytes) {
|
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
function normalizeRegistry(value) {
|
|
23
|
-
if (
|
|
24
|
-
return new Map(
|
|
20
|
+
if (value == null) return null;
|
|
21
|
+
if (Array.isArray(value)) return new Map(value.map((x) => [String(x).toLowerCase().replace(/\.exe$/, ""), String(x)]));
|
|
22
|
+
return new Map(Object.entries(value || {}).map(([name, command]) => [String(name).toLowerCase().replace(/\.exe$/, ""), String(command)]));
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
function resolveExecutable(registry, requested) {
|
|
28
26
|
const raw = String(requested || "").trim();
|
|
29
27
|
if (!raw) fail("INVALID_ARGUMENT", "command is required");
|
|
28
|
+
if (raw.includes("/") || raw.includes("\\") || raw.includes(":") || raw === "." || raw === "..") {
|
|
29
|
+
fail("LOCAL_EXEC_NOT_ALLOWED", "Executable must be a bare command name resolved by the operating system");
|
|
30
|
+
}
|
|
31
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(raw)) fail("LOCAL_EXEC_NOT_ALLOWED", "Executable name is invalid");
|
|
30
32
|
const key = raw.toLowerCase().replace(/\.exe$/, "");
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
if (
|
|
33
|
+
if (BLOCKED_SHELL_EXECUTABLES.has(key)) fail("LOCAL_EXEC_NOT_ALLOWED", "Shell executables are not allowed");
|
|
34
|
+
if (registry) {
|
|
35
|
+
if (registry.has(key)) return { name: key, command: registry.get(key) };
|
|
36
|
+
fail("LOCAL_EXEC_NOT_ALLOWED", "Local executable is not allowed by configured policy");
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
return { name: key, command: raw };
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export class ProcessManager {
|