@whyvix/remote 0.3.20 → 0.3.21

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@whyvix/remote",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@whyvix/remote",
9
- "version": "0.3.20",
9
+ "version": "0.3.21",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/ext-apps": "^2.0.0",
12
12
  "@modelcontextprotocol/sdk": "^1.30.0",
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@whyvix/remote",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
7
7
  "packages/agent/src",
8
8
  "packages/cli/index.js",
9
+ "packages/cli/update-supervisor.js",
9
10
  "packages/cli/browser.js",
10
11
  "packages/cli/presenter.js",
11
12
  "packages/protocol/src",
@@ -28,7 +29,7 @@
28
29
  "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",
29
30
  "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",
30
31
  "test:r10-observability": "node scripts/r10-write-preview-test.js && node scripts/r10-cli-presentation-test.js && node scripts/r10-usage-metering-test.js",
31
- "test:r10-self-update": "node scripts/r10-self-update-manager-test.js && node scripts/r10-update-supervisor-test.js && node scripts/r10-update-lifecycle-test.js && node scripts/r10-update-activation-plan-test.js && node scripts/r10-update-bootstrap-executor-test.js && node scripts/r10-update-supervisor-host-test.js && node scripts/r10-update-archive-policy-test.js && node scripts/r10-update-persistence-test.js && node scripts/r10-windows-detached-survival-test.js"
32
+ "test:r10-self-update": "node scripts/r10-self-update-manager-test.js && node scripts/r10-update-supervisor-test.js && node scripts/r10-update-lifecycle-test.js && node scripts/r10-update-activation-plan-test.js && node scripts/r10-update-bootstrap-executor-test.js && node scripts/r10-update-supervisor-host-test.js && node scripts/r10-update-archive-policy-test.js && node scripts/r10-update-persistence-test.js && node scripts/r10-rollback-runtime-snapshot-test.js && node scripts/r10-windows-detached-survival-test.js"
32
33
  },
33
34
  "dependencies": {
34
35
  "@modelcontextprotocol/ext-apps": "^2.0.0",
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
5
+
6
+ function fail(code,message){throw Object.assign(new Error(message),{code});}
7
+ function sha256(bytes){return crypto.createHash("sha256").update(bytes).digest("hex");}
8
+ function inside(root,file){const rel=path.relative(path.resolve(root),path.resolve(file));return rel&&!rel.startsWith("..")&&!path.isAbsolute(rel);}
9
+
10
+ export class RollbackRuntimeSnapshot{
11
+ constructor({homeDir,nodeExecPath=process.execPath,spawnFn=spawn,audit=()=>{}}={}){
12
+ if(!homeDir)fail("UPDATE_SNAPSHOT_INVALID","Home directory is required");
13
+ this.homeDir=homeDir;this.nodeExecPath=nodeExecPath;this.spawnFn=spawnFn;this.audit=audit;
14
+ }
15
+ snapshotPath(version){return path.join(this.homeDir,".whyvix-remote","updates",String(version),"runtime-snapshot.json");}
16
+ async verify({version}={}){
17
+ const file=this.snapshotPath(version);let snapshot;
18
+ try{snapshot=JSON.parse(await readFile(file,"utf8"));}catch{fail("UPDATE_SNAPSHOT_INVALID","Rollback runtime snapshot is missing or invalid");}
19
+ if(snapshot?.schema!==1||snapshot.version!==version||!snapshot.packageRoot||!snapshot.launcher||!snapshot.packageLock||!snapshot.packageLockSha256||!snapshot.criticalFiles)fail("UPDATE_SNAPSHOT_INVALID","Rollback runtime snapshot identity is invalid");
20
+ if(!inside(snapshot.sourceRoot,snapshot.packageRoot)||!inside(snapshot.packageRoot,snapshot.launcher)||!inside(snapshot.sourceRoot,snapshot.packageLock))fail("UPDATE_SNAPSHOT_INVALID","Rollback runtime snapshot paths escape their trusted roots");
21
+ const lock=await readFile(snapshot.packageLock);if(sha256(lock)!==snapshot.packageLockSha256)fail("UPDATE_SNAPSHOT_TAMPERED","Rollback runtime lockfile hash mismatch");
22
+ const meta=JSON.parse(await readFile(path.join(snapshot.packageRoot,"package.json"),"utf8"));
23
+ if(meta?.name!=="@whyvix/remote"||meta?.version!==version||meta?.bin?.["whyvix-remote"]!=="packages/cli/index.js")fail("UPDATE_SNAPSHOT_INVALID","Rollback runtime package identity is invalid");
24
+ const modulesRoot=path.join(snapshot.sourceRoot,"node_modules");for(const name of Object.keys(meta.dependencies||{})){const parts=name.startsWith("@")?name.split("/"): [name];try{await access(path.join(modulesRoot,...parts));}catch{fail("UPDATE_SNAPSHOT_DEPENDENCY_MISSING",`Rollback runtime dependency is missing: ${name}`);}}
25
+ for(const [relative,expected] of Object.entries(snapshot.criticalFiles)){
26
+ const target=path.resolve(snapshot.packageRoot,relative);if(!inside(snapshot.packageRoot,target))fail("UPDATE_SNAPSHOT_INVALID","Rollback critical file escapes package root");
27
+ const bytes=await readFile(target);if(sha256(bytes)!==expected)fail("UPDATE_SNAPSHOT_TAMPERED",`Rollback runtime critical file changed: ${relative}`);
28
+ }
29
+ await access(snapshot.launcher);
30
+ const fingerprint=sha256(Buffer.from(JSON.stringify({schema:snapshot.schema,version:snapshot.version,sourceRoot:snapshot.sourceRoot,packageRoot:snapshot.packageRoot,launcher:snapshot.launcher,packageLock:snapshot.packageLock,packageLockSha256:snapshot.packageLockSha256,criticalFiles:snapshot.criticalFiles})));
31
+ return{...snapshot,fingerprint};
32
+ }
33
+ async launch({version,cwd=process.cwd()}={}){
34
+ const snapshot=await this.verify({version});
35
+ const child=this.spawnFn(this.nodeExecPath,[snapshot.launcher,"remote"],{cwd,windowsHide:true,shell:false,detached:true,stdio:"ignore",env:{...process.env}});
36
+ if(!Number.isInteger(child?.pid)||child.pid<=0)fail("UPDATE_ROLLBACK_START_FAILED","Rollback runtime did not return a valid pid");
37
+ child.unref?.();this.audit({event:"update.rollback_runtime_started",version,pid:child.pid,fingerprint:snapshot.fingerprint});
38
+ return{version,pid:child.pid,launcher:snapshot.launcher,snapshotFingerprint:snapshot.fingerprint};
39
+ }
40
+ }
@@ -7,18 +7,18 @@ function semver(value){const v=String(value||"").trim();if(!/^(?:0|[1-9]\d*)\.(?
7
7
  function canonical(value){return JSON.stringify(value,Object.keys(value).sort());}
8
8
 
9
9
  export class UpdateActivationPlan{
10
- constructor({manager,homeDir,now=()=>Date.now(),audit=()=>{}}={}){
10
+ constructor({manager,rollbackRuntime=null,homeDir,now=()=>Date.now(),audit=()=>{}}={}){
11
11
  if(!manager)fail("UPDATE_MANAGER_REQUIRED","Self-update manager is required");
12
- this.manager=manager;this.homeDir=homeDir||manager.homeDir;this.now=now;this.audit=audit;
12
+ this.manager=manager;this.rollbackRuntime=rollbackRuntime;this.homeDir=homeDir||manager.homeDir;this.now=now;this.audit=audit;
13
13
  }
14
14
  planPath(){return path.join(this.homeDir,".whyvix-remote","activation-plan.json");}
15
15
  async create({currentVersion,targetVersion,deviceId}={}){
16
16
  currentVersion=semver(currentVersion);targetVersion=semver(targetVersion);
17
17
  if(currentVersion===targetVersion)fail("UPDATE_TARGET_SAME","Target version must differ from current version");
18
18
  if(!String(deviceId||"").trim())fail("UPDATE_DEVICE_INVALID","Device id is required");
19
- const current=await this.manager.verifyPrepared({version:currentVersion});
19
+ const current=this.rollbackRuntime?await this.rollbackRuntime.verify({version:currentVersion}):await this.manager.verifyPrepared({version:currentVersion});
20
20
  const target=await this.manager.verifyPrepared({version:targetVersion});
21
- const plan={schema:1,deviceId:String(deviceId),currentVersion,targetVersion,currentIntegrity:current.integrity,targetIntegrity:target.integrity,createdAt:this.now()};
21
+ const plan={schema:this.rollbackRuntime?2:1,deviceId:String(deviceId),currentVersion,targetVersion,currentIntegrity:current.integrity||null,currentSnapshotFingerprint:current.fingerprint||null,targetIntegrity:target.integrity,createdAt:this.now()};
22
22
  const fingerprint=crypto.createHash("sha256").update(canonical(plan)).digest("hex");
23
23
  const envelope={...plan,fingerprint};
24
24
  await writeJsonAtomic(this.planPath(),envelope);
@@ -29,13 +29,15 @@ export class UpdateActivationPlan{
29
29
  const envelope=await readJsonStrict(this.planPath(),{missing:null,invalidCode:"UPDATE_PLAN_INVALID"});
30
30
  if(!envelope)fail("UPDATE_PLAN_INVALID","Activation plan is missing");
31
31
  const {fingerprint,...plan}=envelope;
32
- if(plan.schema!==1||!String(plan.deviceId||"").trim())fail("UPDATE_PLAN_INVALID","Activation plan identity is invalid");
32
+ if(![1,2].includes(plan.schema)||!String(plan.deviceId||"").trim())fail("UPDATE_PLAN_INVALID","Activation plan identity is invalid");
33
33
  plan.currentVersion=semver(plan.currentVersion);plan.targetVersion=semver(plan.targetVersion);
34
34
  const expected=crypto.createHash("sha256").update(canonical(plan)).digest("hex");
35
35
  if(fingerprint!==expected)fail("UPDATE_PLAN_INVALID","Activation plan fingerprint mismatch");
36
- const current=await this.manager.verifyPrepared({version:plan.currentVersion});
36
+ const current=plan.schema===2&&this.rollbackRuntime?await this.rollbackRuntime.verify({version:plan.currentVersion}):await this.manager.verifyPrepared({version:plan.currentVersion});
37
37
  const target=await this.manager.verifyPrepared({version:plan.targetVersion});
38
- if(current.integrity!==plan.currentIntegrity||target.integrity!==plan.targetIntegrity)fail("UPDATE_PLAN_INVALID","Prepared package integrity no longer matches activation plan");
38
+ if(plan.schema===2&&current.fingerprint!==plan.currentSnapshotFingerprint)fail("UPDATE_PLAN_INVALID","Rollback runtime snapshot no longer matches activation plan");
39
+ if(plan.schema===1&&current.integrity!==plan.currentIntegrity)fail("UPDATE_PLAN_INVALID","Prepared current package integrity no longer matches activation plan");
40
+ if(target.integrity!==plan.targetIntegrity)fail("UPDATE_PLAN_INVALID","Prepared target package integrity no longer matches activation plan");
39
41
  return envelope;
40
42
  }
41
43
  }
@@ -6,9 +6,9 @@ import { acquireExclusiveLock, recoverStaleExclusiveLock } from "./update-persis
6
6
  function fail(code,message){throw Object.assign(new Error(message),{code});}
7
7
 
8
8
  export class UpdateBootstrapExecutor{
9
- constructor({manager,activationPlan,supervisor,waitStable,recoverLock=recoverStaleExclusiveLock,audit=()=>{}}={}){
9
+ constructor({manager,activationPlan,supervisor,rollbackRuntime=null,waitStable,recoverLock=recoverStaleExclusiveLock,audit=()=>{}}={}){
10
10
  if(!manager||!activationPlan||!supervisor||!waitStable)fail("UPDATE_BOOTSTRAP_INVALID","Complete bootstrap dependencies are required");
11
- Object.assign(this,{manager,activationPlan,supervisor,waitStable,recoverLock,audit});
11
+ Object.assign(this,{manager,activationPlan,supervisor,rollbackRuntime,waitStable,recoverLock,audit});
12
12
  }
13
13
  async execute(){
14
14
  const lockPath=path.join(this.manager.homeDir,".whyvix-remote","update-activation.lock");
@@ -31,7 +31,7 @@ export class UpdateBootstrapExecutor{
31
31
  startCandidate:({version})=>this.manager.launchReplacement({version}),
32
32
  waitRegistered:({version,candidate})=>this.manager.waitForRegistration({version,candidate,planFingerprint:plan.fingerprint}),
33
33
  waitStable:args=>this.waitStable(args),
34
- startRollback:({version})=>this.manager.launchReplacement({version}),
34
+ startRollback:({version,authority})=>authority==="runtime_snapshot"&&this.rollbackRuntime?this.rollbackRuntime.launch({version}):this.manager.launchReplacement({version}),
35
35
  audit:this.audit
36
36
  });
37
37
  this.audit({event:"update.bootstrap_started",currentVersion:plan.currentVersion,targetVersion:plan.targetVersion,deviceId:plan.deviceId});
@@ -5,14 +5,16 @@ import { SelfUpdateManager } from "./self-update-manager.js";
5
5
  import { UpdateSupervisor } from "./update-supervisor.js";
6
6
  import { UpdateActivationPlan } from "./update-activation-plan.js";
7
7
  import { UpdateBootstrapExecutor } from "./update-bootstrap-executor.js";
8
+ import { RollbackRuntimeSnapshot } from "./rollback-runtime-snapshot.js";
8
9
 
9
10
  const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
10
11
  function fail(code,message){throw Object.assign(new Error(message),{code});}
11
12
 
12
13
  export async function runUpdateSupervisorHost({homeDir,nodeExecPath=process.execPath,spawnFn=spawn,graceMs=15000,pollMs=250,audit=()=>{}}={}){
13
14
  const manager=new SelfUpdateManager({homeDir,nodeExecPath,spawnFn,audit});
14
- const activationPlan=new UpdateActivationPlan({manager,homeDir,audit});
15
- const supervisor=new UpdateSupervisor({manager,homeDir,audit});
15
+ const rollbackRuntime=new RollbackRuntimeSnapshot({homeDir:homeDir||manager.homeDir,nodeExecPath,spawnFn,audit});
16
+ const activationPlan=new UpdateActivationPlan({manager,rollbackRuntime,homeDir,audit});
17
+ const supervisor=new UpdateSupervisor({manager,rollbackRuntime,homeDir,audit});
16
18
  const waitStable=async({candidate})=>{
17
19
  const pid=Number(candidate?.pid);
18
20
  if(!Number.isInteger(pid)||pid<=0)fail("UPDATE_PID_INVALID","Candidate pid is required for stability monitoring");
@@ -23,7 +25,7 @@ export async function runUpdateSupervisorHost({homeDir,nodeExecPath=process.exec
23
25
  }
24
26
  return true;
25
27
  };
26
- return new UpdateBootstrapExecutor({manager,activationPlan,supervisor,waitStable,audit}).execute();
28
+ return new UpdateBootstrapExecutor({manager,activationPlan,supervisor,rollbackRuntime,waitStable,audit}).execute();
27
29
  }
28
30
 
29
31
  export function launchDetachedUpdateSupervisorHost({nodeExecPath=process.execPath,spawnFn=spawn}={}){
@@ -5,9 +5,9 @@ function fail(code,message){throw Object.assign(new Error(message),{code});}
5
5
  function version(value){const v=String(value||"").trim();if(!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(v))fail("UPDATE_VERSION_INVALID","Explicit semver version is required");return v;}
6
6
 
7
7
  export class UpdateSupervisor{
8
- constructor({manager,homeDir,audit=()=>{},now=()=>Date.now()}={}){
8
+ constructor({manager,rollbackRuntime=null,homeDir,audit=()=>{},now=()=>Date.now()}={}){
9
9
  if(!manager)fail("UPDATE_MANAGER_REQUIRED","Self-update manager is required");
10
- this.manager=manager;this.homeDir=homeDir||manager.homeDir;this.audit=audit;this.now=now;
10
+ this.manager=manager;this.rollbackRuntime=rollbackRuntime;this.homeDir=homeDir||manager.homeDir;this.audit=audit;this.now=now;
11
11
  }
12
12
  statePath(){return path.join(this.homeDir,".whyvix-remote","update-state.json");}
13
13
  async readState(){return readJsonStrict(this.statePath(),{missing:null,invalidCode:"UPDATE_STATE_INVALID"});}
@@ -15,7 +15,7 @@ export class UpdateSupervisor{
15
15
  async arm({currentVersion,targetVersion}={}){
16
16
  currentVersion=version(currentVersion);targetVersion=version(targetVersion);
17
17
  if(currentVersion===targetVersion)fail("UPDATE_TARGET_SAME","Target version must differ from current version");
18
- await this.manager.verifyPrepared({version:currentVersion});
18
+ if(this.rollbackRuntime)await this.rollbackRuntime.verify({version:currentVersion});else await this.manager.verifyPrepared({version:currentVersion});
19
19
  await this.manager.verifyPrepared({version:targetVersion});
20
20
  const state={status:"armed",currentVersion,targetVersion,armedAt:this.now()};
21
21
  await this.writeState(state);this.audit({event:"update.armed",currentVersion,targetVersion});return state;
@@ -43,10 +43,10 @@ export class UpdateSupervisor{
43
43
  async rollbackPlan(){
44
44
  const state=await this.readState();
45
45
  if(!state?.currentVersion)fail("UPDATE_ROLLBACK_UNAVAILABLE","No rollback version is recorded");
46
- await this.manager.verifyPrepared({version:state.currentVersion});
46
+ if(this.rollbackRuntime)await this.rollbackRuntime.verify({version:state.currentVersion});else await this.manager.verifyPrepared({version:state.currentVersion});
47
47
  const reason=state.status||"unknown";
48
48
  await this.writeState({...state,status:"rollback_started",rollbackVersion:state.currentVersion,rollbackStartedAt:this.now()});
49
49
  this.audit({event:"update.rollback_planned",version:state.currentVersion,reason});
50
- return{version:state.currentVersion,reason};
50
+ return{version:state.currentVersion,reason,authority:this.rollbackRuntime?"runtime_snapshot":"prepared_package"};
51
51
  }
52
52
  }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runUpdateSupervisorHost } from "../agent/src/update-supervisor-host.js";
3
+
4
+ runUpdateSupervisorHost().then(
5
+ ()=>process.exit(0),
6
+ error=>{console.error("[update-supervisor]",error?.code||"UPDATE_FAILED",error?.message||String(error));process.exit(1);}
7
+ );