@whyvix/remote 0.3.14 → 0.3.16
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyvix/remote",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test:r3-r5": "node scripts/r3-r5-test.js",
|
|
26
26
|
"test:r10-gui-window-focus": "node scripts/r10-gui-window-action-contract-test.js && node scripts/r10-gui-window-action-source-test.js && node scripts/r10-gui-window-action-agent-capability-test.js && node scripts/r10-gui-window-action-authorization-test.js && node scripts/r10-gui-window-action-loader-test.js && node scripts/r10-gui-window-action-mcp-schema-test.js && node scripts/r10-gui-window-action-cli-wiring-test.js && node scripts/r10-deployment-certify-authorization-test.js && node scripts/r10-deployment-certify-protocol-test.js && node scripts/r10-deployment-activation-response-test.js",
|
|
27
27
|
"test:r10-documents": "node scripts/r10-precision-document-inspect-test.js && node scripts/r10-precision-document-compare-test.js && node scripts/r10-precision-docx-mutation-test.js && node scripts/r10-precision-xlsx-mutation-test.js && node scripts/r10-precision-pptx-mutation-test.js",
|
|
28
|
-
"test:r10-observability": "node scripts/r10-write-preview-test.js && node scripts/r10-cli-presentation-test.js"
|
|
28
|
+
"test:r10-observability": "node scripts/r10-write-preview-test.js && node scripts/r10-cli-presentation-test.js && node scripts/r10-usage-metering-test.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/ext-apps": "^2.0.0",
|
|
@@ -3,6 +3,13 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_EXECUTABLES = null;
|
|
6
|
+
const WINDOWS_TOOLCHAIN_PATTERNS = [
|
|
7
|
+
"C:/wamp64/bin/mysql/*/bin",
|
|
8
|
+
"C:/wamp/bin/mysql/*/bin",
|
|
9
|
+
"C:/xampp/mysql/bin",
|
|
10
|
+
"C:/Program Files/MySQL/*/bin",
|
|
11
|
+
"C:/Program Files/MariaDB */bin"
|
|
12
|
+
];
|
|
6
13
|
const BLOCKED_SHELL_EXECUTABLES = new Set([
|
|
7
14
|
"cmd", "command", "powershell", "pwsh", "sh", "bash", "dash", "zsh", "fish", "wsl"
|
|
8
15
|
]);
|
|
@@ -23,6 +30,24 @@ function normalizeRegistry(value) {
|
|
|
23
30
|
return new Map(Object.entries(value || {}).map(([name, command]) => [String(name).toLowerCase().replace(/\.exe$/, ""), String(command)]));
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
function expandWindowsRoots(patterns) {
|
|
34
|
+
const roots=[];
|
|
35
|
+
for(const pattern of patterns){
|
|
36
|
+
if(!pattern.includes("*")){roots.push(pattern);continue;}
|
|
37
|
+
const before=pattern.slice(0,pattern.indexOf("*")),after=pattern.slice(pattern.indexOf("*")+1),base=before.replace(/[\\/]$/,"");
|
|
38
|
+
try{for(const entry of fs.readdirSync(base,{withFileTypes:true}))if(entry.isDirectory())roots.push(path.join(base,entry.name,after));}catch{}
|
|
39
|
+
}
|
|
40
|
+
return roots;
|
|
41
|
+
}
|
|
42
|
+
function executableMatches(executable,{platform,pathValue,pathExtValue,toolchainPatterns=WINDOWS_TOOLCHAIN_PATTERNS}={}){
|
|
43
|
+
const dirs=String(pathValue??process.env.PATH??"").split(path.delimiter).filter(Boolean);
|
|
44
|
+
if(platform==="win32")dirs.push(...expandWindowsRoots(toolchainPatterns));
|
|
45
|
+
const extensions=platform==="win32"?(path.extname(executable.command)?[""]:String(pathExtValue??process.env.PATHEXT??".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)):[""];
|
|
46
|
+
const matches=[];
|
|
47
|
+
for(const dir of dirs)for(const ext of extensions){const candidate=path.resolve(dir,executable.command+ext);try{const stat=fs.statSync(candidate);if(stat.isFile()&&!matches.some(x=>x.toLowerCase()===candidate.toLowerCase()))matches.push(candidate);}catch{}}
|
|
48
|
+
return matches.slice(0,16);
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
function resolveExecutable(registry, requested) {
|
|
27
52
|
const raw = String(requested || "").trim();
|
|
28
53
|
if (!raw) fail("INVALID_ARGUMENT", "command is required");
|
|
@@ -40,8 +65,8 @@ function resolveExecutable(registry, requested) {
|
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
export class ProcessManager {
|
|
43
|
-
constructor({ audit=()=>{}, spawnFn=spawn, allowedExecutables=DEFAULT_EXECUTABLES, platform=process.platform, nodeExecPath=process.execPath, maxOutputBytes=262144, maxRetainedJobs=100, maxRunningJobs=16, jobRetentionMs=300000, now=()=>Date.now() }={}) {
|
|
44
|
-
this.audit=audit; this.spawnFn=spawnFn; this.executableRegistry=normalizeRegistry(allowedExecutables); this.platform=platform; this.nodeExecPath=nodeExecPath;
|
|
68
|
+
constructor({ audit=()=>{}, spawnFn=spawn, allowedExecutables=DEFAULT_EXECUTABLES, platform=process.platform, nodeExecPath=process.execPath, toolchainPatterns=WINDOWS_TOOLCHAIN_PATTERNS, maxOutputBytes=262144, maxRetainedJobs=100, maxRunningJobs=16, jobRetentionMs=300000, now=()=>Date.now() }={}) {
|
|
69
|
+
this.audit=audit; this.spawnFn=spawnFn; this.executableRegistry=normalizeRegistry(allowedExecutables); this.platform=platform; this.nodeExecPath=nodeExecPath; this.toolchainPatterns=toolchainPatterns;
|
|
45
70
|
this.maxOutputBytes=Math.max(1024,Math.min(Number(maxOutputBytes)||262144,1048576));
|
|
46
71
|
this.maxRetainedJobs=Math.max(1,Math.min(Number(maxRetainedJobs)||100,1000));
|
|
47
72
|
this.maxRunningJobs=Math.max(1,Math.min(Number(maxRunningJobs)||16,128));
|
|
@@ -58,14 +83,11 @@ export class ProcessManager {
|
|
|
58
83
|
|
|
59
84
|
discover({command,pathValue=process.env.PATH||"",pathExtValue=process.env.PATHEXT||".COM;.EXE;.BAT;.CMD"}={}) {
|
|
60
85
|
const executable=resolveExecutable(this.executableRegistry,command);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const matches=[];
|
|
64
|
-
for(const dir of dirs)for(const ext of extensions){const candidate=path.resolve(dir,executable.command+ext);try{const stat=fs.statSync(candidate);if(stat.isFile()&&!matches.includes(candidate))matches.push(candidate);}catch{}}
|
|
65
|
-
return{command:executable.name,available:matches.length>0,matches:matches.slice(0,16)};
|
|
86
|
+
const matches=executableMatches(executable,{platform:this.platform,pathValue,pathExtValue,toolchainPatterns:this.toolchainPatterns});
|
|
87
|
+
return{command:executable.name,available:matches.length>0,matches};
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
start({command,args=[],cwd,timeoutMs=30000,ownerId}={}) {
|
|
90
|
+
start({command,args=[],cwd,timeoutMs=30000,ownerId,executableMatch}={}) {
|
|
69
91
|
this.#cleanup();
|
|
70
92
|
const executable=resolveExecutable(this.executableRegistry,command);
|
|
71
93
|
if(!Array.isArray(args)||args.some(x=>typeof x!=="string"))fail("INVALID_ARGUMENT","args must be an array of strings");
|
|
@@ -74,9 +96,22 @@ export class ProcessManager {
|
|
|
74
96
|
args=["--no-install",...args];
|
|
75
97
|
}
|
|
76
98
|
let spawnCommand=executable.command;
|
|
99
|
+
if(executableMatch!=null){
|
|
100
|
+
const requested=path.resolve(String(executableMatch));
|
|
101
|
+
const matches=executableMatches(executable,{platform:this.platform,toolchainPatterns:this.toolchainPatterns});
|
|
102
|
+
const selected=matches.find(x=>path.resolve(x).toLowerCase()===requested.toLowerCase());
|
|
103
|
+
if(!selected)fail("LOCAL_EXEC_NOT_ALLOWED","Selected executable is not an approved discovery match");
|
|
104
|
+
spawnCommand=selected;
|
|
105
|
+
}
|
|
77
106
|
if(this.platform==="win32"&&(executable.name==="npm"||executable.name==="npx")) {
|
|
78
|
-
const
|
|
79
|
-
|
|
107
|
+
const selectedDir=executableMatch!=null?path.dirname(spawnCommand):path.dirname(this.nodeExecPath);
|
|
108
|
+
const selectedNode=path.join(selectedDir,"node.exe");
|
|
109
|
+
const selectedCli=path.join(selectedDir,"node_modules","npm","bin",executable.name==="npm"?"npm-cli.js":"npx-cli.js");
|
|
110
|
+
const fallbackCli=path.join(path.dirname(this.nodeExecPath),"node_modules","npm","bin",executable.name==="npm"?"npm-cli.js":"npx-cli.js");
|
|
111
|
+
if(executableMatch!=null){
|
|
112
|
+
try{if(!fs.statSync(selectedNode).isFile()||!fs.statSync(selectedCli).isFile())fail("LOCAL_EXEC_NOT_ALLOWED","Selected Windows command shim does not have an approved adjacent Node launcher");}catch(error){if(error?.code==="LOCAL_EXEC_NOT_ALLOWED")throw error;fail("LOCAL_EXEC_NOT_ALLOWED","Selected Windows command shim does not have an approved adjacent Node launcher");}
|
|
113
|
+
spawnCommand=selectedNode;args=[selectedCli,...args];
|
|
114
|
+
}else{spawnCommand=this.nodeExecPath;args=[fallbackCli,...args];}
|
|
80
115
|
}
|
|
81
116
|
if([...this.processes.values()].filter(x=>x.running).length>=this.maxRunningJobs)fail("LOCAL_JOB_LIMIT","Local running job limit reached");
|
|
82
117
|
const id=String(this.nextId++);let child;
|
|
@@ -93,6 +128,7 @@ export class ProcessManager {
|
|
|
93
128
|
|
|
94
129
|
#owned(id,ownerId){this.#cleanup();const s=this.processes.get(String(id));if(!s)fail("LOCAL_JOB_NOT_FOUND","Local job not found");if(s.ownerId!=null&&String(ownerId||"")!==s.ownerId)fail("LOCAL_JOB_NOT_FOUND","Local job not found");return s;}
|
|
95
130
|
output({id,ownerId}={}){const s=this.#owned(id,ownerId);return{id:s.id,stdout:s.stdout,stderr:s.stderr,running:s.running,exitCode:s.exitCode,errorCode:s.errorCode,truncated:s.truncated,timedOut:s.timedOut};}
|
|
131
|
+
async wait({id,ownerId,waitMs=30000}={}){const s=this.#owned(id,ownerId),bounded=Math.max(0,Math.min(Number(waitMs)||0,30000));if(!s.running||bounded===0)return this.output({id,ownerId});await new Promise(resolve=>{let settled=false;const done=()=>{if(settled)return;settled=true;clearTimeout(timer);s.child.off?.("close",done);s.child.off?.("error",done);resolve();};const timer=setTimeout(done,bounded);s.child.once?.("close",done);s.child.once?.("error",done);if(!s.running)done();});return this.output({id,ownerId});}
|
|
96
132
|
input({id,ownerId,data,end=false}={}){const s=this.#owned(id,ownerId);if(!s.running)fail("LOCAL_JOB_NOT_RUNNING","Local job is not running");if(typeof data!=="string")fail("INVALID_ARGUMENT","data must be a string");if(Buffer.byteLength(data,"utf8")>65536)fail("CONTENT_TOO_LARGE","stdin data exceeds 65536 UTF-8 bytes");if(!s.child.stdin||typeof s.child.stdin.write!=="function")fail("LOCAL_STDIN_UNAVAILABLE","Local process stdin is unavailable");if(data.length)s.child.stdin.write(data);if(end){if(typeof s.child.stdin.end!=="function")fail("LOCAL_STDIN_UNAVAILABLE","Local process stdin cannot be closed");s.child.stdin.end();}this.audit({event:"process.input",id,bytes:Buffer.byteLength(data,"utf8"),end:Boolean(end)});return{id:s.id,writtenBytes:Buffer.byteLength(data,"utf8"),ended:Boolean(end)};}
|
|
97
133
|
stop({id,ownerId}={}){const s=this.#owned(id,ownerId);if(s.running)s.child.kill();return{id:s.id,stopping:s.running};}
|
|
98
134
|
}
|
|
@@ -129,7 +129,8 @@ export function createToolExecutor({ security, processManager, remoteExecutionMa
|
|
|
129
129
|
if(tool==="remote.stop"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_JOB_NOT_FOUND"});return remoteExecutionManager.stop(args);}
|
|
130
130
|
if(tool==="process.discover")return processManager.discover(args);
|
|
131
131
|
if(tool==="process.start"){const cwd=security.resolveFilesystemPath(args.cwd);return processManager.start({...args,cwd});}
|
|
132
|
-
if(tool==="process.output")return processManager.output(args);
|
|
132
|
+
if(tool==="process.output")return processManager.output(args);
|
|
133
|
+
if(tool==="process.wait")return processManager.wait(args);
|
|
133
134
|
if(tool==="process.input")return processManager.input(args);
|
|
134
135
|
if(tool==="process.stop")return processManager.stop(args);
|
|
135
136
|
if(tool==="git.status"||tool==="git.diff"||tool==="git.log"){const cwd=security.resolveFilesystemPath(args.cwd);const gitArgs=tool==="git.status"?["status","--porcelain=v1","--branch"]:tool==="git.diff"?["diff","--",...(args.paths||[])]:["log","--oneline","-n",String(Math.min(Number(args.limit||10),50))];const {stdout,stderr}=await execFileAsync("git",gitArgs,{cwd,windowsHide:true,maxBuffer:1024*1024});return{stdout,stderr};}
|