@whyvix/remote 0.3.8 → 0.3.10
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/native/gui-window-action-helper-manifest.json +8 -0
- package/native/win32-x64/whyvix-gui-window-action-helper.exe +0 -0
- package/package.json +6 -3
- package/packages/agent/src/agent.js +5 -3
- package/packages/agent/src/deployment-manager.js +3 -2
- package/packages/agent/src/deployment-protocol.js +2 -2
- package/packages/agent/src/gui-window-action-backend-loader.js +3 -0
- package/packages/agent/src/gui-window-action-manager.js +6 -0
- package/packages/agent/src/tools.js +2 -0
- package/packages/agent/src/windows-gui-window-action-backend.js +6 -0
- package/packages/agent/src/windows-gui-window-action-runner.js +2 -0
- package/packages/cli/index.js +4 -1
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyvix/remote",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
"packages/protocol/src",
|
|
11
11
|
"native/gui-helper-manifest.json",
|
|
12
12
|
"native/win32-x64/whyvix-gui-helper.exe",
|
|
13
|
+
"native/gui-window-action-helper-manifest.json",
|
|
14
|
+
"native/win32-x64/whyvix-gui-window-action-helper.exe",
|
|
13
15
|
"README.md"
|
|
14
16
|
],
|
|
15
17
|
"scripts": {
|
|
16
18
|
"gateway": "node services/gateway/src/index.js",
|
|
17
19
|
"agent": "node packages/agent/src/index.js",
|
|
18
20
|
"test:r0": "node scripts/r0-test.js",
|
|
19
|
-
"test": "npm run test:r0 && npm run test:r1-r2 && npm run test:r3-r5",
|
|
21
|
+
"test": "npm run test:r0 && npm run test:r1-r2 && npm run test:r3-r5 && npm run test:r10-gui-window-focus",
|
|
20
22
|
"check": "node --check packages/protocol/src/index.js && node --check packages/agent/src/security.js && node --check packages/agent/src/process-manager.js && node --check packages/agent/src/tools.js && node --check packages/agent/src/agent.js && node --check packages/agent/src/index.js && node --check packages/cli/index.js && node --check services/gateway/src/device-registry.js && node --check services/gateway/src/gateway.js && node --check services/gateway/src/index.js && node --check scripts/r0-test.js && node --check scripts/r1-r2-test.js && node --check scripts/r3-r5-test.js",
|
|
21
23
|
"test:r1-r2": "node scripts/r1-r2-test.js",
|
|
22
|
-
"test:r3-r5": "node scripts/r3-r5-test.js"
|
|
24
|
+
"test:r3-r5": "node scripts/r3-r5-test.js",
|
|
25
|
+
"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"
|
|
23
26
|
},
|
|
24
27
|
"dependencies": {
|
|
25
28
|
"@modelcontextprotocol/ext-apps": "^2.0.0",
|
|
@@ -7,17 +7,19 @@ import { ProcessManager } from "./process-manager.js";
|
|
|
7
7
|
import { RemoteExecutionManager } from "./remote-execution-manager.js";
|
|
8
8
|
import { DeploymentManager } from "./deployment-manager.js";
|
|
9
9
|
import { GuiManager } from "./gui-manager.js";
|
|
10
|
+
import { GuiWindowActionManager } from "./gui-window-action-manager.js";
|
|
10
11
|
import { createToolExecutor } from "./tools.js";
|
|
11
12
|
|
|
12
|
-
export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUID(),deviceName=os.hostname(),token="",workspaces=[process.cwd()],remoteTargets=[],deploymentProfiles=[],deploymentTransferArtifact=null,guiBackend=null,agentVersion="unknown",capabilities=[],reconnectMs=1000,reconnect=true,heartbeatMs=30000,audit=()=>{},log=console.log,errorLog=console.error,onRegistered=()=>{}}={}) {
|
|
13
|
+
export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUID(),deviceName=os.hostname(),token="",workspaces=[process.cwd()],remoteTargets=[],deploymentProfiles=[],deploymentTransferArtifact=null,guiBackend=null,guiWindowActionBackend=null,agentVersion="unknown",capabilities=[],reconnectMs=1000,reconnect=true,heartbeatMs=30000,audit=()=>{},log=console.log,errorLog=console.error,onRegistered=()=>{}}={}) {
|
|
13
14
|
let ws=null,reconnectTimer=null,stopped=false,generation=0;
|
|
14
15
|
const security=createSecurity({token,workspaces,audit});
|
|
15
16
|
const processManager=new ProcessManager({audit});
|
|
16
17
|
const remoteExecutionManager=new RemoteExecutionManager({targets:remoteTargets,audit});
|
|
17
18
|
const deploymentManager=new DeploymentManager({profiles:deploymentProfiles,security,audit,transferArtifact:deploymentTransferArtifact});
|
|
18
19
|
const guiManager=new GuiManager({backend:guiBackend,audit});
|
|
19
|
-
const
|
|
20
|
-
const
|
|
20
|
+
const guiWindowActionManager=new GuiWindowActionManager({backend:guiWindowActionBackend,audit});
|
|
21
|
+
const advertisedCapabilities=[...new Set([...capabilities,...(guiManager.capabilities().available?["gui.observe"]:[]),...(guiWindowActionManager.capabilities().available?["gui.window.focus"]:[])])].sort();
|
|
22
|
+
const executeTool=createToolExecutor({security,processManager,remoteExecutionManager,deploymentManager,guiManager,guiWindowActionManager});
|
|
21
23
|
function scheduleReconnect(reason){
|
|
22
24
|
if(stopped||!reconnect||reconnectTimer)return;
|
|
23
25
|
log("[agent:reconnect-scheduled]",reason,reconnectMs);
|
|
@@ -24,6 +24,7 @@ export class DeploymentManager{
|
|
|
24
24
|
this.security=security;this.audit=audit;this.execFileFn=execFileFn;this.transferArtifact=transferArtifact;
|
|
25
25
|
}
|
|
26
26
|
listProfiles(){return [...this.profiles.values()].map(({alias,label="",remoteAlias})=>({alias,label,remoteAlias}));}
|
|
27
|
+
resolveOperation({profile:profileAlias,operation,commit}){return buildDeploymentOperation(this.#profile(profileAlias),{operation,commit});}
|
|
27
28
|
#profile(alias){const value=String(alias||"");if(!PROFILE_RE.test(value)||!this.profiles.has(value))fail("DEPLOY_PROFILE_DENIED","Deployment profile is not registered");return this.profiles.get(value);}
|
|
28
29
|
async prepareLocal({profile:profileAlias,commit,baseCommit=null}){
|
|
29
30
|
const profile=this.#profile(profileAlias);
|
|
@@ -33,8 +34,8 @@ export class DeploymentManager{
|
|
|
33
34
|
const {stdout:rootRaw}=await this.execFileFn("git",["rev-parse","--show-toplevel"],{cwd:repo,windowsHide:true,maxBuffer:1024*1024});
|
|
34
35
|
const root=this.security.resolveAllowed(rootRaw.trim());
|
|
35
36
|
if(path.resolve(root)!==path.resolve(repo))fail("DEPLOY_PROFILE_DENIED","Deployment repository root does not match profile");
|
|
36
|
-
const {stdout:status}=await this.execFileFn("git",["status","--porcelain"],{cwd:root,windowsHide:true,maxBuffer:1024*1024});
|
|
37
|
-
const dirty=status.split(/\r?\n/).filter(Boolean)
|
|
37
|
+
const {stdout:status}=await this.execFileFn("git",["status","--porcelain","--untracked-files=no"],{cwd:root,windowsHide:true,maxBuffer:1024*1024});
|
|
38
|
+
const dirty=status.split(/\r?\n/).filter(Boolean);
|
|
38
39
|
if(dirty.length)fail("DEPLOY_REPOSITORY_DIRTY","Deployment repository must be clean");
|
|
39
40
|
try{await this.execFileFn("git",["cat-file","-e",commit+"^{commit}"],{cwd:root,windowsHide:true,maxBuffer:1024*1024});}catch{fail("DEPLOY_COMMIT_INVALID","Deployment commit does not exist locally");}
|
|
40
41
|
let dependencyChanged=false;
|
|
@@ -22,9 +22,9 @@ export function releaseIdFromCommit(commit){
|
|
|
22
22
|
|
|
23
23
|
export function buildDeployHelperArgs(input){
|
|
24
24
|
const operation=String(input?.operation||""),commit=String(input?.commit||""),releaseId=releaseIdFromCommit(commit);
|
|
25
|
-
if(!["verify","prepare","status"].includes(operation))fail("DEPLOY_OPERATION_DENIED","Deployment helper operation is not allowed");
|
|
25
|
+
if(!["verify","prepare","certify","status","activate"].includes(operation))fail("DEPLOY_OPERATION_DENIED","Deployment helper operation is not allowed");
|
|
26
26
|
if(input?.releaseId!==undefined&&String(input.releaseId)!==releaseId)fail("DEPLOY_ARGUMENT_INVALID","Release identity does not match commit");
|
|
27
|
-
if(
|
|
27
|
+
if(["status","activate","certify"].includes(operation)){if(input?.artifactSha256!==undefined)fail("DEPLOY_ARGUMENT_INVALID",operation+" does not accept artifact checksum");return Object.freeze([operation,releaseId,commit]);}
|
|
28
28
|
const artifactSha256=String(input?.artifactSha256||"");if(!HASH_RE.test(artifactSha256))fail("DEPLOY_CHECKSUM_INVALID","Artifact SHA-256 is required");
|
|
29
29
|
return Object.freeze([operation,releaseId,commit,artifactSha256]);
|
|
30
30
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import path from "node:path";import { access,readFile } from "node:fs/promises";import { createReadStream } from "node:fs";import { createHash } from "node:crypto";import { fileURLToPath } from "node:url";import { WindowsGuiWindowActionBackend } from "./windows-gui-window-action-backend.js";
|
|
2
|
+
async function sha256(file){const hash=createHash("sha256");for await(const chunk of createReadStream(file))hash.update(chunk);return hash.digest("hex");}
|
|
3
|
+
export async function loadGuiWindowActionBackend({platform=process.platform,arch=process.arch,runAction,packageRoot}={}){if(platform!=="win32")return null;const root=packageRoot??path.resolve(path.dirname(fileURLToPath(import.meta.url)),"..","..",".."),key=`${platform}-${arch}`,manifestPath=path.join(root,"native","gui-window-action-helper-manifest.json");let manifest;try{manifest=JSON.parse(await readFile(manifestPath,"utf8"));}catch{return null;}const entry=manifest?.artifacts?.[key];if(!entry||typeof entry.path!=="string"||!/^[a-f0-9]{64}$/.test(entry.sha256??""))return null;const helperPath=path.resolve(root,entry.path);if(!helperPath.startsWith(path.resolve(root)+path.sep))return null;try{await access(helperPath);}catch{return null;}if(await sha256(helperPath)!==entry.sha256||typeof runAction!=="function")return null;return new WindowsGuiWindowActionBackend({helperPath,runAction});}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
function fail(message,code){throw Object.assign(new Error(message),{code});}
|
|
2
|
+
export class GuiWindowActionManager{
|
|
3
|
+
constructor({backend=null,audit=()=>{}}={}){this.backend=backend;this.audit=audit;}
|
|
4
|
+
capabilities(){return{available:Boolean(this.backend),focus:Boolean(this.backend?.focusWindow)};}
|
|
5
|
+
async focusWindow({windowId}={}){const id=String(windowId??"");if(!/^\d{1,20}$/.test(id))fail("Invalid GUI window id","GUI_WINDOW_INVALID");if(typeof this.backend?.focusWindow!=="function")fail("GUI window action is not available","GUI_WINDOW_ACTION_NOT_AVAILABLE");const result=await this.backend.focusWindow({windowId:id});this.audit({event:"gui.window.focus",windowId:id});return result;}
|
|
6
|
+
}
|
|
@@ -21,6 +21,7 @@ export function createToolExecutor({ security, processManager, remoteExecutionMa
|
|
|
21
21
|
if(tool==="gui.windows")return guiManager.windows();
|
|
22
22
|
if(tool==="gui.foreground")return guiManager.foreground();
|
|
23
23
|
if(tool==="gui.capture")return guiManager.capture(args);
|
|
24
|
+
if(tool==="gui.window.focus"){if(!guiWindowActionManager)throw Object.assign(new Error("GUI window action manager is not configured"),{code:"GUI_WINDOW_ACTION_NOT_AVAILABLE"});return guiWindowActionManager.focusWindow(args);}
|
|
24
25
|
if(tool==="fs.begin_binary_write")return binaryTransferManager.begin(args);
|
|
25
26
|
if(tool==="fs.write_binary_session")return binaryTransferManager.write(args);
|
|
26
27
|
if(tool==="fs.finalize_binary_write")return binaryTransferManager.finalize(args);
|
|
@@ -116,6 +117,7 @@ export function createToolExecutor({ security, processManager, remoteExecutionMa
|
|
|
116
117
|
if(tool==="remote.targets"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return{targets:remoteExecutionManager.listTargets()};}
|
|
117
118
|
if(tool==="remote.start"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return remoteExecutionManager.start(args);}
|
|
118
119
|
if(tool==="remote.deploy_status"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return remoteExecutionManager.startDeployHelper({alias:args.alias,input:{operation:"status",commit:args.commit},timeoutMs:args.timeoutMs,ownerId:args.ownerId});}
|
|
120
|
+
if(tool==="deploy.status_release"||tool==="deploy.certify_release"||tool==="deploy.activate_release"){if(!remoteExecutionManager||!deploymentManager)throw Object.assign(new Error("Deployment authority is not configured"),{code:"DEPLOY_PROFILE_DENIED"});const operation=tool==="deploy.status_release"?"status":tool==="deploy.certify_release"?"certify":"activate",resolved=deploymentManager.resolveOperation({profile:args.profile,operation,commit:args.commit});return remoteExecutionManager.startDeployHelper({alias:resolved.remoteAlias,input:{operation,commit:resolved.commit},timeoutMs:args.timeoutMs,ownerId:args.ownerId});}
|
|
119
121
|
if(tool==="remote.output"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_JOB_NOT_FOUND"});return remoteExecutionManager.output(args);}
|
|
120
122
|
if(tool==="remote.input"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_JOB_NOT_FOUND"});return remoteExecutionManager.input(args);}
|
|
121
123
|
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);}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
function fail(message,code){throw Object.assign(new Error(message),{code});}
|
|
3
|
+
export class WindowsGuiWindowActionBackend{
|
|
4
|
+
constructor({helperPath,runAction}={}){if(typeof runAction!=="function"||typeof helperPath!=="string"||!helperPath.trim())fail("Windows GUI window action helper is not configured","GUI_WINDOW_ACTION_NOT_CONFIGURED");this.name="windows-native-window-action";this.helperPath=path.resolve(helperPath);this.runAction=runAction;}
|
|
5
|
+
async focusWindow({windowId}){const response=await this.runAction({helperPath:this.helperPath,windowId});if(!response||typeof response!=="object"||Array.isArray(response))fail("Windows GUI window action returned invalid payload","GUI_WINDOW_ACTION_PROTOCOL_ERROR");if(response.error)fail(String(response.error.message||"GUI window action rejected"),String(response.error.code||"GUI_WINDOW_ACTION_FAILED"));return response;}
|
|
6
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
export function runWindowsGuiWindowAction({helperPath,windowId}){return new Promise((resolve,reject)=>execFile(helperPath,["focus",String(windowId)],{windowsHide:true,timeout:5000,maxBuffer:65536,encoding:"utf8"},(error,stdout)=>{if(error)return reject(Object.assign(new Error("Windows GUI window action failed"),{code:error.killed?"GUI_WINDOW_ACTION_TIMEOUT":"GUI_WINDOW_ACTION_FAILED"}));try{resolve(JSON.parse(stdout));}catch{reject(Object.assign(new Error("Windows GUI window action returned invalid JSON"),{code:"GUI_WINDOW_ACTION_PROTOCOL_ERROR"}));}}));}
|
package/packages/cli/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { createAgent } from "../agent/src/agent.js";
|
|
|
8
8
|
import { loadDeploymentProfiles } from "../agent/src/deployment-profile-loader.js";
|
|
9
9
|
import { transferDeploymentArtifact } from "../agent/src/deployment-transfer-runner.js";
|
|
10
10
|
import { loadGuiBackend } from "../agent/src/gui-backend-loader.js";
|
|
11
|
-
import { runWindowsGuiHelper } from "../agent/src/windows-gui-helper-runner.js";
|
|
11
|
+
import { runWindowsGuiHelper } from "../agent/src/windows-gui-helper-runner.js";
|
|
12
|
+
import { loadGuiWindowActionBackend } from "../agent/src/gui-window-action-backend-loader.js";
|
|
13
|
+
import { runWindowsGuiWindowAction } from "../agent/src/windows-gui-window-action-runner.js";
|
|
12
14
|
import { openBrowser } from "./browser.js";
|
|
13
15
|
|
|
14
16
|
const packageRoot=join(dirname(fileURLToPath(import.meta.url)),"..","..");
|
|
@@ -23,6 +25,7 @@ const configuredDeploymentProfiles=String(process.env.WHYVIX_DEPLOY_PROFILES_FIL
|
|
|
23
25
|
const defaultDeploymentProfiles=join(os.homedir(),".whyvix-remote","deployment-profiles.json");
|
|
24
26
|
const deploymentProfiles=await loadDeploymentProfiles(configuredDeploymentProfiles||defaultDeploymentProfiles,{optional:!configuredDeploymentProfiles});
|
|
25
27
|
const guiBackend=await loadGuiBackend({packageRoot,runHelper:runWindowsGuiHelper});
|
|
28
|
+
const guiWindowActionBackend=await loadGuiWindowActionBackend({packageRoot,runAction:runWindowsGuiWindowAction});
|
|
26
29
|
const capabilities=["filesystem.crud","process.bounded","git.read","git.mutate",...(remoteTargets.length?["remote.bounded"]:[]),...(deploymentProfiles.length?["deployment.profiled"]:[])];
|
|
27
30
|
const deviceId=process.env.WHYVIX_DEVICE_ID||crypto.createHash("sha256").update(os.hostname()+"|"+os.userInfo().username).digest("hex").slice(0,24);
|
|
28
31
|
const credentialDir=join(os.homedir(),".whyvix-remote");
|