@whyvix/remote 0.3.12 → 0.3.13

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.12",
3
+ "version": "0.3.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -18,15 +18,18 @@
18
18
  "gateway": "node services/gateway/src/index.js",
19
19
  "agent": "node packages/agent/src/index.js",
20
20
  "test:r0": "node scripts/r0-test.js",
21
- "test": "npm run test:r0 && npm run test:r1-r2 && npm run test:r3-r5 && npm run test:r10-gui-window-focus",
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
+ "test": "npm run test:r0 && npm run test:r1-r2 && npm run test:r3-r5 && npm run test:r10-gui-window-focus && npm run test:r10-documents && npm run test:r10-observability",
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/precision-document-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",
23
23
  "test:r1-r2": "node scripts/r1-r2-test.js",
24
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 && 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"
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 && 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",
26
+ "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",
27
+ "test:r10-observability": "node scripts/r10-write-preview-test.js"
26
28
  },
27
29
  "dependencies": {
28
30
  "@modelcontextprotocol/ext-apps": "^2.0.0",
29
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
+ "fflate": "0.8.3",
30
33
  "ws": "^8.18.0",
31
34
  "zod": "^4.6.5"
32
35
  },
@@ -8,8 +8,31 @@ import { RemoteExecutionManager } from "./remote-execution-manager.js";
8
8
  import { DeploymentManager } from "./deployment-manager.js";
9
9
  import { GuiManager } from "./gui-manager.js";
10
10
  import { GuiWindowActionManager } from "./gui-window-action-manager.js";
11
+ import { PrecisionDocumentManager } from "./precision-document-manager.js";
11
12
  import { createToolExecutor } from "./tools.js";
12
13
 
14
+ const WRITE_PREVIEW_MAX_LINES=24;
15
+ const WRITE_PREVIEW_MAX_CHARS=4000;
16
+ function sensitiveWritePath(value=""){
17
+ const name=String(value).replace(/\\/g,"/").split("/").pop()?.toLowerCase()||"";
18
+ return name===".env"||name.startsWith(".env.")||/\.(?:pem|key|p12|pfx|keystore)$/.test(name)||/(?:credential|credentials|secret|secrets|token|password|passwd)/.test(name);
19
+ }
20
+ function redactPreviewText(value){
21
+ return String(value).replace(/((?:password|passwd|secret|token|api[_-]?key|authorization)\s*[:=]\s*)([^\s"'\x60]+)/gi,"$1[REDACTED]");
22
+ }
23
+ function boundedPreview(value){
24
+ const text=redactPreviewText(value),lines=text.split(/\r?\n/),visible=lines.slice(0,WRITE_PREVIEW_MAX_LINES);
25
+ let preview=visible.join("\n");if(preview.length>WRITE_PREVIEW_MAX_CHARS)preview=preview.slice(0,WRITE_PREVIEW_MAX_CHARS);
26
+ const omittedLines=Math.max(0,lines.length-visible.length),omittedChars=Math.max(0,text.length-preview.length);
27
+ return preview+(omittedLines||omittedChars?`\n… preview truncated (${omittedLines} lines, ${omittedChars} chars omitted)`:"");
28
+ }
29
+ export function writePreview(tool,args={}){
30
+ if(!["fs.write_text","fs.create_text","fs.replace_text"].includes(tool))return null;
31
+ const target=String(args.path||"");if(sensitiveWritePath(target))return `[write-preview] ${tool} ${target}\n[REDACTED: sensitive file]`;
32
+ if(tool==="fs.replace_text")return `[write-preview] ${tool} ${target}\n--- expected\n+++ replacement\n- ${boundedPreview(args.expected??"")}\n+ ${boundedPreview(args.replacement??"")}`;
33
+ return `[write-preview] ${tool} ${target}\n${boundedPreview(args.content??"")}`;
34
+ }
35
+
13
36
  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=()=>{}}={}) {
14
37
  let ws=null,reconnectTimer=null,stopped=false,generation=0;
15
38
  const security=createSecurity({token,workspaces,audit});
@@ -18,8 +41,9 @@ export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUI
18
41
  const deploymentManager=new DeploymentManager({profiles:deploymentProfiles,security,audit,transferArtifact:deploymentTransferArtifact});
19
42
  const guiManager=new GuiManager({backend:guiBackend,audit});
20
43
  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});
44
+ const precisionDocumentManager=new PrecisionDocumentManager({security,audit});
45
+ const advertisedCapabilities=[...new Set([...capabilities,...(guiManager.capabilities().available?["gui.observe"]:[]),...(guiWindowActionManager.capabilities().available?["gui.window.focus"]:[]),...(precisionDocumentManager.capabilities().available?["document.precision.inspect","document.precision.compare","document.precision.mutate.docx","document.precision.mutate.xlsx","document.precision.mutate.pptx"]:[])])].sort();
46
+ const executeTool=createToolExecutor({security,processManager,remoteExecutionManager,deploymentManager,guiManager,guiWindowActionManager,precisionDocumentManager});
23
47
  function scheduleReconnect(reason){
24
48
  if(stopped||!reconnect||reconnectTimer)return;
25
49
  log("[agent:reconnect-scheduled]",reason,reconnectMs);
@@ -43,7 +67,7 @@ export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUI
43
67
  }
44
68
  log("[agent:connecting]",gatewayUrl,"generation="+current);
45
69
  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:advertisedCapabilities}})));});
46
- 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);}});
70
+ 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});const preview=writePreview(m.tool,m.arguments);if(preview)log(preview);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);}});
47
71
  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");});
48
72
  socket.on("error",(e)=>{errorLog("[agent:socket-error]",e.message);scheduleReconnect("error");});
49
73
  }
@@ -0,0 +1,110 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { unzipSync, zipSync, strToU8 } from "fflate";
5
+
6
+ function fail(message,code){throw Object.assign(new Error(message),{code});}
7
+ function count(xml,re){return (xml.match(re)||[]).length;}
8
+ function text(parts,name){const value=parts[name];return value?Buffer.from(value).toString("utf8"):"";}
9
+ function names(parts,prefix,re){return Object.keys(parts).filter(n=>n.startsWith(prefix)&&re.test(n));}
10
+ function hashBuffer(buffer){return crypto.createHash("sha256").update(buffer).digest("hex");}
11
+ function partDigest(parts,name){const value=parts[name];return value?hashBuffer(Buffer.from(value)):null;}
12
+ function partsDigest(parts,names){const hash=crypto.createHash("sha256");for(const name of [...names].sort()){hash.update(name);hash.update(Buffer.from(parts[name]||[]));}return names.length?hash.digest("hex"):null;}
13
+ function formulaDigest(parts,names){const formulas=[];const re=/<f\b[^>]*>[\s\S]*?<\/f>|<f\b[^>]*\/>/g;for(const name of [...names].sort()){for(const value of text(parts,name).match(re)||[])formulas.push(name+":"+value);}return formulas.length?hashBuffer(Buffer.from(formulas.join("\n"),"utf8")):null;}
14
+ function relCount(parts,prefix=""){return Object.keys(parts).filter(n=>n.startsWith(prefix)&&/\.rels$/i.test(n)).reduce((n,k)=>n+count(text(parts,k),/<Relationship\b/g),0);}
15
+ function macroInfo(parts){const macroParts=Object.keys(parts).filter(n=>/vbaProject\.bin$/i.test(n));return{present:macroParts.length>0,parts:macroParts};}
16
+ function signatureInfo(parts){const signatureParts=Object.keys(parts).filter(n=>/^_xmlsignatures\//i.test(n));return{present:signatureParts.length>0,parts:signatureParts};}
17
+ function xmlEscape(value){return String(value).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;");}
18
+ function paragraphRanges(xml){const ranges=[];const re=/<w:p(?:\s[^>]*)?>[\s\S]*?<\/w:p>/g;let m;while((m=re.exec(xml)))ranges.push({start:m.index,end:re.lastIndex,xml:m[0]});return ranges;}
19
+ function patchParagraphProperties(paragraph,format={}){const allowed=new Set(["alignment","spaceAfterPt","firstLineIndentPt","leftIndentPt","hangingIndentPt"]);for(const k of Object.keys(format))if(!allowed.has(k))fail(`Unsupported DOCX paragraph format field: ${k}`,"DOCUMENT_MUTATION_NOT_ALLOWED");const map={left:"left",center:"center",right:"right",justify:"both"};let props=(paragraph.match(/<w:pPr(?:\s[^>]*)?>[\s\S]*?<\/w:pPr>/)||[])[0]||"<w:pPr></w:pPr>";if(format.alignment!==undefined){if(!map[format.alignment])fail("Invalid DOCX paragraph alignment","DOCUMENT_MUTATION_INVALID");props=props.replace(/<w:jc\b[^>]*\/?>(?:<\/w:jc>)?/g,"");props=props.replace("</w:pPr>",`<w:jc w:val="${map[format.alignment]}"/></w:pPr>`);}if(format.spaceAfterPt!==undefined){const tw=Math.round(Number(format.spaceAfterPt)*20);if(!Number.isFinite(tw)||tw<0)fail("Invalid DOCX paragraph spacing","DOCUMENT_MUTATION_INVALID");props=props.replace(/<w:spacing\b[^>]*\/?>(?:<\/w:spacing>)?/g,"");props=props.replace("</w:pPr>",`<w:spacing w:after="${tw}"/></w:pPr>`);}const ind={};for(const [key,attr] of [["firstLineIndentPt","firstLine"],["leftIndentPt","left"],["hangingIndentPt","hanging"]])if(format[key]!==undefined){const tw=Math.round(Number(format[key])*20);if(!Number.isFinite(tw)||tw<0)fail("Invalid DOCX paragraph indentation","DOCUMENT_MUTATION_INVALID");ind[attr]=tw;}if(Object.keys(ind).length){props=props.replace(/<w:ind\b[^>]*\/?>(?:<\/w:ind>)?/g,"");props=props.replace("</w:pPr>",`<w:ind ${Object.entries(ind).map(([k,v])=>`w:${k}="${xmlEscape(v)}"`).join(" ")}/></w:pPr>`);}return paragraph.includes("<w:pPr")?paragraph.replace(/<w:pPr(?:\s[^>]*)?>[\s\S]*?<\/w:pPr>/,props):paragraph.replace(/^(<w:p(?:\s[^>]*)?>)/,`$1${props}`);}
20
+ function authorityEqual(before,after,ignore=[]){const skip=new Set(ignore);return Object.keys(before.authorityDigests).filter(k=>!skip.has(k)).every(k=>before.authorityDigests[k]===after.authorityDigests[k]);}
21
+ function pptParagraphRanges(xml){const ranges=[];const re=/<a:p(?:\s[^>]*)?>[\s\S]*?<\/a:p>/g;let m;while((m=re.exec(xml)))ranges.push({start:m.index,end:re.lastIndex,xml:m[0]});return ranges;}
22
+ function patchPptAlignment(paragraph,alignment){const map={left:"l",center:"ctr",right:"r",justify:"just"};const value=map[alignment];if(!value)fail("Invalid PPTX paragraph alignment","DOCUMENT_MUTATION_INVALID");const ppr=paragraph.match(/<a:pPr(?:\s[^>]*)?(?:\/>|>[\s\S]*?<\/a:pPr>)/)?.[0];if(!ppr)return paragraph.replace(/^(<a:p(?:\s[^>]*)?>)/,`$1<a:pPr algn="${value}"/>`);const patched=/\salgn="[^"]*"/.test(ppr)?ppr.replace(/\salgn="[^"]*"/,` algn="${value}"`):ppr.replace(/^<a:pPr\b/,`<a:pPr algn="${value}"`);return paragraph.replace(ppr,patched);}
23
+ function patchCellStyle(cell,styleIndex){const index=Number(styleIndex);if(!Number.isInteger(index)||index<0)fail("Invalid XLSX cell style index","DOCUMENT_MUTATION_INVALID");if(cell.includes("<f")||cell.includes(":f"))fail("Formula cells are read-only in XLSX format mutation","DOCUMENT_MUTATION_NOT_ALLOWED");const marker=' s="';const at=cell.indexOf(marker);if(at>=0){const end=cell.indexOf('"',at+marker.length);if(end<0)fail("Malformed XLSX cell style","DOCUMENT_MUTATION_INVALID");return cell.slice(0,at)+marker+index+cell.slice(end);}const close=cell.indexOf(">");if(close<0)fail("Malformed XLSX cell","DOCUMENT_MUTATION_INVALID");return cell.slice(0,close)+' s="'+index+'"'+cell.slice(close);}
24
+
25
+
26
+ function docxSnapshot(parts){
27
+ const document=text(parts,"word/document.xml"),styles=text(parts,"word/styles.xml"),numbering=text(parts,"word/numbering.xml"),settings=text(parts,"word/settings.xml");
28
+ const headers=names(parts,"word/",/^word\/header\d+\.xml$/i),footers=names(parts,"word/",/^word\/footer\d+\.xml$/i);
29
+ return{kind:"docx",structure:{
30
+ paragraphs:count(document,/<w:p(?:\s|\/?>)/g),tables:count(document,/<w:tbl(?:\s|\/?>)/g),sections:count(document,/<w:sectPr(?:\s|\/?>)/g),
31
+ styles:count(styles,/<w:style(?:\s|\/?>)/g),numberingAbstract:count(numbering,/<w:abstractNum(?:\s|\/?>)/g),numberingInstances:count(numbering,/<w:num(?:\s|\/?>)/g),
32
+ headers:headers.length,footers:footers.length,footnotes:text(parts,"word/footnotes.xml")?count(text(parts,"word/footnotes.xml"),/<w:footnote(?:\s|\/?>)/g):0,
33
+ endnotes:text(parts,"word/endnotes.xml")?count(text(parts,"word/endnotes.xml"),/<w:endnote(?:\s|\/?>)/g):0,
34
+ comments:text(parts,"word/comments.xml")?count(text(parts,"word/comments.xml"),/<w:comment(?:\s|\/?>)/g):0,
35
+ bookmarks:count(document,/<w:bookmarkStart(?:\s|\/?>)/g),fields:count(document,/<w:(?:fldSimple|instrText)(?:\s|\/?>)/g),
36
+ drawings:count(document,/<w:(?:drawing|pict)(?:\s|\/?>)/g),relationships:relCount(parts,"word/"),trackChanges:count(document,/<w:(?:ins|del|moveFrom|moveTo)(?:\s|\/?>)/g),
37
+ evenOddHeaders:/<w:evenAndOddHeaders(?:\s|\/>)/.test(settings),titlePage:count(document,/<w:titlePg(?:\s|\/>)/g)
38
+ },authorityDigests:{styles:partDigest(parts,"word/styles.xml"),numbering:partDigest(parts,"word/numbering.xml"),settings:partDigest(parts,"word/settings.xml")}};
39
+ }
40
+ function xlsxSnapshot(parts){
41
+ const workbook=text(parts,"xl/workbook.xml"),worksheets=names(parts,"xl/worksheets/",/^xl\/worksheets\/sheet\d+\.xml$/i),tables=names(parts,"xl/tables/",/^xl\/tables\/table\d+\.xml$/i),charts=names(parts,"xl/charts/",/^xl\/charts\/chart\d+\.xml$/i);
42
+ let formulas=0,formulaErrors=0,merges=0,validations=0,conditionalFormats=0;
43
+ for(const n of worksheets){const x=text(parts,n);formulas+=count(x,/<f(?:\s|\/?>)/g);formulaErrors+=count(x,/<f[^>]*>[^<]*#REF!/g);merges+=count(x,/<mergeCell(?:\s|\/?>)/g);validations+=count(x,/<dataValidation(?:\s|\/?>)/g);conditionalFormats+=count(x,/<conditionalFormatting(?:\s|\/?>)/g);}
44
+ return{kind:"xlsx",structure:{sheets:worksheets.length,formulas,formulaRefErrors:formulaErrors,definedNames:count(workbook,/<definedName(?:\s|\/?>)/g),tables:tables.length,charts:charts.length,mergedCells:merges,dataValidations:validations,conditionalFormats,pivotTables:names(parts,"xl/pivotTables/",/\.xml$/i).length,pivotCaches:names(parts,"xl/pivotCache/",/\.xml$/i).length,externalLinks:names(parts,"xl/externalLinks/",/\.xml$/i).length,relationships:relCount(parts,"xl/"),calcChain:Boolean(parts["xl/calcChain.xml"])},authorityDigests:{workbook:partDigest(parts,"xl/workbook.xml"),styles:partDigest(parts,"xl/styles.xml"),formulas:formulaDigest(parts,worksheets)}};
45
+ }
46
+ function pptxSnapshot(parts){
47
+ const presentation=text(parts,"ppt/presentation.xml"),slides=names(parts,"ppt/slides/",/^ppt\/slides\/slide\d+\.xml$/i),masters=names(parts,"ppt/slideMasters/",/^ppt\/slideMasters\/slideMaster\d+\.xml$/i),layouts=names(parts,"ppt/slideLayouts/",/^ppt\/slideLayouts\/slideLayout\d+\.xml$/i);
48
+ let shapes=0,tables=0,charts=0,hyperlinks=0;
49
+ for(const n of slides){const x=text(parts,n);shapes+=count(x,/<p:(?:sp|pic|graphicFrame)(?:\s|\/?>)/g);tables+=count(x,/<a:tbl(?:\s|\/?>)/g);charts+=count(x,/<c:chart(?:\s|\/?>)/g);hyperlinks+=count(x,/<a:hlink(?:Click|Hover)(?:\s|\/?>)/g);}
50
+ return{kind:"pptx",structure:{slides:slides.length,masters:masters.length,layouts:layouts.length,notes:names(parts,"ppt/notesSlides/",/^ppt\/notesSlides\/notesSlide\d+\.xml$/i).length,themes:names(parts,"ppt/theme/",/\.xml$/i).length,shapes,tables,chartReferences:charts,chartParts:names(parts,"ppt/charts/",/\.xml$/i).length,hyperlinks,relationships:relCount(parts,"ppt/"),sections:count(presentation,/<p14:section(?:\s|\/?>)/g)},authorityDigests:{presentation:partDigest(parts,"ppt/presentation.xml"),masters:partsDigest(parts,masters),layouts:partsDigest(parts,layouts)}};
51
+ }
52
+ function detect(parts,ext){
53
+ if(parts["word/document.xml"])return"docx";
54
+ if(parts["xl/workbook.xml"])return"xlsx";
55
+ if(parts["ppt/presentation.xml"])return"pptx";
56
+ if([".docx",".docm",".xlsx",".xlsm",".pptx",".pptm"].includes(ext))fail("Office package structure does not match its extension","DOCUMENT_INVALID_PACKAGE");
57
+ fail("Unsupported precision document format","DOCUMENT_FORMAT_NOT_SUPPORTED");
58
+ }
59
+ export class PrecisionDocumentManager{
60
+ constructor({security,audit=()=>{}}={}){this.security=security;this.audit=audit;}
61
+ capabilities(){return{available:true,precision:"strict",preserveUnknown:true,minimalMutation:true,formats:{docx:{inspect:true,mutate:true,operations:["formatParagraph"]},xlsx:{inspect:true,formulaIntegrity:true,mutate:true,operations:["formatCell"]},pptx:{inspect:true,mutate:true,operations:["formatParagraph"]},pdf:{inspect:false,mutate:false}}};}
62
+ async inspect(args={}){
63
+ const target=this.security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);if(!stat.isFile())fail("Target must be a regular file","INVALID_TARGET");
64
+ const maxBytes=100*1024*1024;if(stat.size>maxBytes)fail("Office package exceeds strict inspection limit","DOCUMENT_TOO_LARGE");
65
+ const buffer=await fs.readFile(target);let parts;try{parts=unzipSync(new Uint8Array(buffer));}catch{fail("Document is not a valid ZIP-based Office package","DOCUMENT_INVALID_PACKAGE");}
66
+ const ext=path.extname(target).toLowerCase(),kind=detect(parts,ext);const snapshot=kind==="docx"?docxSnapshot(parts):kind==="xlsx"?xlsxSnapshot(parts):pptxSnapshot(parts);
67
+ const result={path:target,size:stat.size,sha256:hashBuffer(buffer),precision:"strict",preserveUnknown:true,package:{parts:Object.keys(parts).length,unknownPartsPreserved:true,macro:macroInfo(parts),signatures:signatureInfo(parts)},...snapshot};
68
+ this.audit({event:"document.inspect",path:target,kind});return result;
69
+ }
70
+ async mutatePptx(args={}){
71
+ const source=this.security.resolveFilesystemPath(args.sourcePath),target=this.security.resolveFilesystemPath(args.targetPath);
72
+ if(source===target)fail("Precision mutation requires a distinct target path","DOCUMENT_MUTATION_TARGET_REQUIRED");
73
+ const before=await this.inspect({path:source});if(before.kind!=="pptx")fail("PPTX mutation requires a PPTX source","DOCUMENT_FORMAT_MISMATCH");
74
+ try{await fs.stat(target);fail("Mutation target already exists","DOCUMENT_TARGET_EXISTS");}catch(error){if(error?.code!=="ENOENT")throw error;}
75
+ if(before.package.signatures.present)fail("Signed Office documents are read-only in precision mutation","DOCUMENT_SIGNATURE_PROTECTED");
76
+ const parts=unzipSync(new Uint8Array(await fs.readFile(source))),mutations=Array.isArray(args.mutations)?args.mutations:[];
77
+ if(!mutations.length)fail("At least one PPTX mutation is required","DOCUMENT_MUTATION_REQUIRED");
78
+ for(const mutation of mutations){if(mutation?.op!=="formatParagraph")fail("Unsupported PPTX mutation operation","DOCUMENT_MUTATION_NOT_ALLOWED");const si=Number(mutation.slideIndex),pi=Number(mutation.paragraphIndex);if(!Number.isInteger(si)||si<0)fail("Invalid PPTX slide index","DOCUMENT_MUTATION_INVALID");const name="ppt/slides/slide"+(si+1)+".xml";let xml=text(parts,name);if(!xml)fail("PPTX slide index is out of range","DOCUMENT_MUTATION_INVALID");const ranges=pptParagraphRanges(xml);if(!Number.isInteger(pi)||pi<0||pi>=ranges.length)fail("PPTX paragraph index is out of range","DOCUMENT_MUTATION_INVALID");const q=ranges[pi];parts[name]=strToU8(xml.slice(0,q.start)+patchPptAlignment(q.xml,mutation.alignment)+xml.slice(q.end));}
79
+ await fs.writeFile(target,Buffer.from(zipSync(parts)));
80
+ let after,comparison;try{after=await this.inspect({path:target});comparison=await this.compare({beforePath:source,afterPath:target});if(comparison.changedStructure.length||!authorityEqual(before,after)||!comparison.packageFlags.macroPreserved||!comparison.packageFlags.signaturesPreserved)fail("Precision PPTX integrity gate rejected mutation","DOCUMENT_INTEGRITY_REJECTED");}catch(error){await fs.rm(target,{force:true});throw error;}
81
+ const result={kind:"pptx",operation:"minimal-mutation",sourcePath:source,targetPath:target,beforeSha256:before.sha256,afterSha256:after.sha256,mutationsApplied:mutations.length,integrity:{structurePreserved:true,authoritiesPreserved:true,macroPreserved:comparison.packageFlags.macroPreserved,signaturesPreserved:comparison.packageFlags.signaturesPreserved}};this.audit({event:"document.mutate",kind:"pptx",sourcePath:source,targetPath:target,mutationsApplied:mutations.length});return result;
82
+ }
83
+ async mutateXlsx(args={}){
84
+ const source=this.security.resolveFilesystemPath(args.sourcePath),target=this.security.resolveFilesystemPath(args.targetPath);if(source===target)fail("Precision mutation requires a distinct target path","DOCUMENT_MUTATION_TARGET_REQUIRED");const before=await this.inspect({path:source});if(before.kind!=="xlsx")fail("XLSX mutation requires an XLSX source","DOCUMENT_FORMAT_MISMATCH");try{await fs.stat(target);fail("Mutation target already exists","DOCUMENT_TARGET_EXISTS");}catch(error){if(error?.code!=="ENOENT")throw error;}if(before.package.signatures.present)fail("Signed Office documents are read-only in precision mutation","DOCUMENT_SIGNATURE_PROTECTED");const input=await fs.readFile(source),parts=unzipSync(new Uint8Array(input));const mutations=Array.isArray(args.mutations)?args.mutations:[];if(!mutations.length)fail("At least one XLSX mutation is required","DOCUMENT_MUTATION_REQUIRED");for(const mutation of mutations){if(mutation?.op!=="formatCell")fail("Unsupported XLSX mutation operation","DOCUMENT_MUTATION_NOT_ALLOWED");const sheetIndex=Number(mutation.sheetIndex);if(!Number.isInteger(sheetIndex)||sheetIndex<0)fail("Invalid XLSX sheet index","DOCUMENT_MUTATION_INVALID");const sheetName=`xl/worksheets/sheet${sheetIndex+1}.xml`;let xml=text(parts,sheetName);if(!xml)fail("XLSX sheet index is out of range","DOCUMENT_MUTATION_INVALID");const ref=String(mutation.cellRef||"").toUpperCase();if(!/^[A-Z]{1,3}[1-9][0-9]*$/.test(ref))fail("Invalid XLSX cell reference","DOCUMENT_MUTATION_INVALID");const cells=xml.match(/<c\b[^>]*(?:\/>|>[\s\S]*?<\/c>)/g)||[];const cell=cells.find(value=>value.includes(`r="${ref}"`));if(!cell)fail("XLSX cell reference was not found","DOCUMENT_MUTATION_INVALID");xml=xml.replace(cell,patchCellStyle(cell,mutation.styleIndex));parts[sheetName]=strToU8(xml);}await fs.writeFile(target,Buffer.from(zipSync(parts)));let after,comparison;try{after=await this.inspect({path:target});comparison=await this.compare({beforePath:source,afterPath:target});const structureOk=comparison.changedStructure.length===0;const authoritiesOk=authorityEqual(before,after,["styles"]);if(!structureOk||!authoritiesOk||before.authorityDigests.formulas!==after.authorityDigests.formulas||!comparison.packageFlags.macroPreserved||!comparison.packageFlags.signaturesPreserved)fail("Precision XLSX integrity gate rejected mutation","DOCUMENT_INTEGRITY_REJECTED");}catch(error){await fs.rm(target,{force:true});throw error;}const result={kind:"xlsx",operation:"minimal-mutation",sourcePath:source,targetPath:target,beforeSha256:before.sha256,afterSha256:after.sha256,mutationsApplied:mutations.length,integrity:{structurePreserved:true,formulaAuthorityPreserved:before.authorityDigests.formulas===after.authorityDigests.formulas,workbookAuthorityPreserved:before.authorityDigests.workbook===after.authorityDigests.workbook,macroPreserved:comparison.packageFlags.macroPreserved,signaturesPreserved:comparison.packageFlags.signaturesPreserved}};this.audit({event:"document.mutate",kind:"xlsx",sourcePath:source,targetPath:target,mutationsApplied:mutations.length});return result;
85
+ }
86
+ async mutateDocx(args={}){
87
+ const source=this.security.resolveFilesystemPath(args.sourcePath),target=this.security.resolveFilesystemPath(args.targetPath);
88
+ if(source===target)fail("Precision mutation requires a distinct target path","DOCUMENT_MUTATION_TARGET_REQUIRED");
89
+ const before=await this.inspect({path:source});if(before.kind!=="docx")fail("DOCX mutation requires a DOCX source","DOCUMENT_FORMAT_MISMATCH");
90
+ try{await fs.stat(target);fail("Mutation target already exists","DOCUMENT_TARGET_EXISTS");}catch(error){if(error?.code!=="ENOENT")throw error;}
91
+ if(before.package.signatures.present)fail("Signed Office documents are read-only in precision mutation","DOCUMENT_SIGNATURE_PROTECTED");
92
+ const input=await fs.readFile(source),parts=unzipSync(new Uint8Array(input));let document=text(parts,"word/document.xml");const mutations=Array.isArray(args.mutations)?args.mutations:[];if(!mutations.length)fail("At least one DOCX mutation is required","DOCUMENT_MUTATION_REQUIRED");
93
+ for(const mutation of mutations){if(mutation?.op!=="formatParagraph")fail("Unsupported DOCX mutation operation","DOCUMENT_MUTATION_NOT_ALLOWED");const ranges=paragraphRanges(document),index=Number(mutation.paragraphIndex);if(!Number.isInteger(index)||index<0||index>=ranges.length)fail("DOCX paragraph index is out of range","DOCUMENT_MUTATION_INVALID");const range=ranges[index],patched=patchParagraphProperties(range.xml,mutation.format||{});document=document.slice(0,range.start)+patched+document.slice(range.end);}
94
+ parts["word/document.xml"]=strToU8(document);await fs.writeFile(target,Buffer.from(zipSync(parts)));
95
+ let after,comparison;try{after=await this.inspect({path:target});comparison=await this.compare({beforePath:source,afterPath:target});const structureOk=comparison.changedStructure.every(x=>x.field==="relationships"&&x.before===x.after);const authoritiesOk=authorityEqual(before,after);if(!structureOk||!authoritiesOk||!comparison.packageFlags.macroPreserved||!comparison.packageFlags.signaturesPreserved)fail("Precision DOCX integrity gate rejected mutation","DOCUMENT_INTEGRITY_REJECTED");}catch(error){await fs.rm(target,{force:true});throw error;}
96
+ const result={kind:"docx",operation:"minimal-mutation",sourcePath:source,targetPath:target,beforeSha256:before.sha256,afterSha256:after.sha256,mutationsApplied:mutations.length,integrity:{structurePreserved:comparison.changedStructure.length===0,authoritiesPreserved:true,macroPreserved:comparison.packageFlags.macroPreserved,signaturesPreserved:comparison.packageFlags.signaturesPreserved}};
97
+ this.audit({event:"document.mutate",kind:"docx",sourcePath:source,targetPath:target,mutationsApplied:mutations.length});return result;
98
+ }
99
+ async compare(args={}){
100
+ const before=await this.inspect({path:args.beforePath});
101
+ const after=await this.inspect({path:args.afterPath});
102
+ if(before.kind!==after.kind)fail("Documents must have the same Office format","DOCUMENT_FORMAT_MISMATCH");
103
+ const changedStructure=Object.keys(before.structure).filter(k=>JSON.stringify(before.structure[k])!==JSON.stringify(after.structure[k])).map(k=>({field:k,before:before.structure[k],after:after.structure[k]}));
104
+ const changedAuthorities=Object.keys(before.authorityDigests).filter(k=>before.authorityDigests[k]!==after.authorityDigests[k]).map(k=>({authority:k,before:before.authorityDigests[k],after:after.authorityDigests[k]}));
105
+ const packageFlags={macroPreserved:JSON.stringify(before.package.macro)===JSON.stringify(after.package.macro),signaturesPreserved:JSON.stringify(before.package.signatures)===JSON.stringify(after.package.signatures)};
106
+ const result={kind:before.kind,precision:"strict",before:{path:before.path,sha256:before.sha256},after:{path:after.path,sha256:after.sha256},changedStructure,changedAuthorities,packageFlags,identical:before.sha256===after.sha256};
107
+ this.audit({event:"document.compare",kind:before.kind,beforePath:before.path,afterPath:after.path});
108
+ return result;
109
+ }
110
+ }
@@ -1,166 +1,172 @@
1
- import fs from "node:fs/promises";
2
- import { createReadStream } from "node:fs";
3
- import { BinaryTransferManager } from "./binary-transfer-manager.js";
4
- import path from "node:path";
5
- import crypto from "node:crypto";
6
- import { execFile } from "node:child_process";
7
- import { promisify } from "node:util";
8
- const execFileAsync=promisify(execFile);
9
-
10
- export function createToolExecutor({ security, processManager, remoteExecutionManager, deploymentManager, guiManager }) {
11
- const binaryTransferManager=new BinaryTransferManager({security,audit:(entry)=>security.audit(entry)});
12
- const pendingDeployments=new Map();
13
- const pendingDeploymentTtlMs=10*60*1000;
14
- function deploymentOwner(args){return String(args.ownerId||"");}
15
- function prunePendingDeployments(){const now=Date.now();for(const [id,state] of pendingDeployments)if(now-state.createdAt>pendingDeploymentTtlMs)pendingDeployments.delete(id);}
16
- return async function executeTool(tool,args={}) {
17
- security.audit({event:"tool.request",tool});
1
+ import fs from "node:fs/promises";
2
+ import { createReadStream } from "node:fs";
3
+ import { BinaryTransferManager } from "./binary-transfer-manager.js";
4
+ import path from "node:path";
5
+ import crypto from "node:crypto";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ const execFileAsync=promisify(execFile);
9
+
10
+ export function createToolExecutor({ security, processManager, remoteExecutionManager, deploymentManager, guiManager, guiWindowActionManager, precisionDocumentManager }) {
11
+ const binaryTransferManager=new BinaryTransferManager({security,audit:(entry)=>security.audit(entry)});
12
+ const pendingDeployments=new Map();
13
+ const pendingDeploymentTtlMs=10*60*1000;
14
+ function deploymentOwner(args){return String(args.ownerId||"");}
15
+ function prunePendingDeployments(){const now=Date.now();for(const [id,state] of pendingDeployments)if(now-state.createdAt>pendingDeploymentTtlMs)pendingDeployments.delete(id);}
16
+ return async function executeTool(tool,args={}) {
17
+ security.audit({event:"tool.request",tool});
18
18
  if(tool==="device.ping") return {pong:true,timestamp:new Date().toISOString()};
19
- if(tool==="gui.capabilities")return guiManager?.capabilities()??{available:false,backend:null,observation:{displays:false,windows:false,foreground:false,capture:false},interaction:false,accessibility:false};
20
- if(tool==="gui.displays")return guiManager.displays();
21
- if(tool==="gui.windows")return guiManager.windows();
22
- if(tool==="gui.foreground")return guiManager.foreground();
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);}
25
- if(tool==="fs.begin_binary_write")return binaryTransferManager.begin(args);
26
- if(tool==="fs.write_binary_session")return binaryTransferManager.write(args);
27
- if(tool==="fs.finalize_binary_write")return binaryTransferManager.finalize(args);
28
- if(tool==="fs.abort_binary_write")return binaryTransferManager.abort(args);
29
- if(tool==="fs.list"){const target=security.resolveFilesystemPath(args.path);const entries=await fs.readdir(target,{withFileTypes:true});return{path:target,entries:entries.map(e=>({name:e.name,type:e.isDirectory()?"directory":e.isFile()?"file":"other"}))};}
30
- if(tool==="fs.read"){const target=security.resolveFilesystemPath(args.path);const max=Math.min(Number(args.maxBytes||65536),262144);const h=await fs.open(target,"r");try{const stat=await h.stat();if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});const readBytes=Math.min(stat.size,262144);const snapshot=Buffer.alloc(readBytes);const {bytesRead}=await h.read(snapshot,0,readBytes,0);const data=snapshot.subarray(0,bytesRead);return{path:target,content:data.subarray(0,max).toString("utf8"),truncated:stat.size>max,size:stat.size,...(stat.size<=262144&&bytesRead===stat.size?{sha256:crypto.createHash("sha256").update(data).digest("hex")}:{})};}finally{await h.close();}}
31
- if(tool==="fs.search"){
32
- const root=security.resolveFilesystemPath(args.path);const q=String(args.query||"");if(!q)throw Object.assign(new Error("query is required"),{code:"INVALID_ARGUMENT"});
33
- const mode=args.mode==="content"?"content":"name",maxResults=Math.min(Math.max(Number(args.maxResults||50),1),200),maxDepth=Math.min(Math.max(Number(args.maxDepth??8),0),20),maxFileBytes=Math.min(Math.max(Number(args.maxFileBytes||262144),1),1048576),matches=[];
34
- const needle=q.toLowerCase();
35
- async function walk(dir,depth){
36
- if(matches.length>=maxResults||depth>maxDepth)return;
37
- const entries=await fs.readdir(dir,{withFileTypes:true});
38
- for(const entry of entries){
39
- if(matches.length>=maxResults)break;
40
- const candidate=path.join(dir,entry.name);let target;try{target=security.resolveFilesystemPath(candidate);}catch{continue;}
41
- if(entry.isDirectory()){if(mode==="name"&&entry.name.toLowerCase().includes(needle))matches.push({name:entry.name,path:target,type:"directory"});await walk(target,depth+1);continue;}
42
- if(!entry.isFile())continue;
43
- if(mode==="name"){if(entry.name.toLowerCase().includes(needle))matches.push({name:entry.name,path:target,type:"file"});continue;}
44
- const stat=await fs.stat(target);if(stat.size>maxFileBytes)continue;
45
- const raw=await fs.readFile(target);if(raw.includes(0))continue;const content=raw.toString("utf8");const lines=content.split(/\r?\n/);
46
- for(let i=0;i<lines.length&&matches.length<maxResults;i++){if(lines[i].toLowerCase().includes(needle))matches.push({name:entry.name,path:target,type:"file",line:i+1,preview:lines[i].slice(0,500)});}
47
- }
48
- }
49
- await walk(root,0);return{path:root,query:q,mode,matches,truncated:matches.length>=maxResults};
50
- }
51
- if(tool==="fs.info"){const target=security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);return{path:target,type:stat.isDirectory()?"directory":stat.isFile()?"file":"other",size:stat.size,createdAt:stat.birthtime.toISOString(),modifiedAt:stat.mtime.toISOString()};}
52
- if(tool==="fs.hash"){const target=security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});const hash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(target);stream.on("data",(chunk)=>hash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});return{path:target,size:stat.size,sha256:hash.digest("hex")};}
53
- if(tool==="fs.read_binary"){const target=security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});const offset=Number(args.offset||0),maxBytes=Number(args.maxBytes||32768);if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(maxBytes)||maxBytes<1||maxBytes>32768||offset>stat.size)throw Object.assign(new Error("Invalid binary range"),{code:"INVALID_ARGUMENT"});const bytesToRead=Math.min(maxBytes,stat.size-offset),handle=await fs.open(target,"r");try{const buffer=Buffer.alloc(bytesToRead),{bytesRead}=await handle.read(buffer,0,bytesToRead,offset);return{path:target,offset,bytesRead,size:stat.size,dataBase64:buffer.subarray(0,bytesRead).toString("base64"),eof:offset+bytesRead>=stat.size};}finally{await handle.close();}}
54
- if(tool==="fs.create_binary"){const target=security.resolveFilesystemPath(args.path),encoded=String(args.dataBase64??"");if(encoded.length>43692||encoded.length%4!==0||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded))throw Object.assign(new Error("Invalid or oversized binary chunk"),{code:"INVALID_ARGUMENT"});const data=Buffer.from(encoded,"base64");if(data.length>32768)throw Object.assign(new Error("Binary chunk exceeds 32768 bytes"),{code:"CONTENT_TOO_LARGE"});try{await fs.writeFile(target,data,{flag:"wx"});}catch(error){if(error?.code==="EEXIST")throw Object.assign(new Error("Target already exists"),{code:"FILE_EXISTS"});throw error;}return{path:target,size:data.length,sha256:crypto.createHash("sha256").update(data).digest("hex")};}
55
- if(tool==="fs.write_binary"){const target=security.resolveFilesystemPath(args.path),offset=Number(args.offset),expectedSize=Number(args.expectedSize),expectedSha256=String(args.expectedSha256||"").toLowerCase(),encoded=String(args.dataBase64??"");if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(expectedSize)||expectedSize<0||offset!==expectedSize||!/^[a-f0-9]{64}$/.test(expectedSha256))throw Object.assign(new Error("Invalid guarded binary write"),{code:"INVALID_ARGUMENT"});if(encoded.length>43692||encoded.length%4!==0||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded))throw Object.assign(new Error("Invalid or oversized binary chunk"),{code:"INVALID_ARGUMENT"});const data=Buffer.from(encoded,"base64");if(data.length>32768)throw Object.assign(new Error("Binary chunk exceeds 32768 bytes"),{code:"CONTENT_TOO_LARGE"});const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});if(stat.size!==expectedSize)throw Object.assign(new Error("File changed since previous chunk"),{code:"FILE_CHANGED"});const hash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(target);stream.on("data",(chunk)=>hash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});if(hash.digest("hex")!==expectedSha256)throw Object.assign(new Error("File changed since previous chunk"),{code:"FILE_CHANGED"});await fs.appendFile(target,data);const updatedHash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(target);stream.on("data",(chunk)=>updatedHash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});return{path:target,size:expectedSize+data.length,sha256:updatedHash.digest("hex")};}
56
- if(tool==="fs.copy"){const source=security.resolveFilesystemPath(args.source),destination=security.resolveFilesystemPath(args.destination),stat=await fs.stat(source);if(!stat.isFile())throw Object.assign(new Error("Source must be a regular file"),{code:"INVALID_TARGET"});try{await fs.copyFile(source,destination,fs.constants?.COPYFILE_EXCL??1);}catch(error){if(error?.code==="EEXIST")throw Object.assign(new Error("Destination already exists"),{code:"FILE_EXISTS"});throw error;}const copied=await fs.stat(destination),hash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(destination);stream.on("data",(chunk)=>hash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});return{source,destination,size:copied.size,sha256:hash.digest("hex")};}
57
- if(tool==="fs.mkdir"){const target=security.resolveFilesystemPath(args.path);try{const stat=await fs.stat(target);if(!stat.isDirectory())throw Object.assign(new Error("Target exists and is not a directory"),{code:"FILE_EXISTS"});return{path:target,created:false};}catch(error){if(error?.code!=="ENOENT")throw error;}await fs.mkdir(target,{recursive:Boolean(args.recursive)});return{path:target,created:true};}
58
- if(tool==="fs.move"){const source=security.resolveFilesystemPath(args.source);const destination=security.resolveFilesystemPath(args.destination);try{await fs.lstat(destination);throw Object.assign(new Error("Destination already exists"),{code:"FILE_EXISTS"});}catch(error){if(error?.code!=="ENOENT")throw error;}await fs.rename(source,destination);return{source,destination};}
59
- if(tool==="fs.delete_file"){const requested=path.resolve(String(args.path||""));const target=security.resolveFilesystemPath(requested);const linkStat=await fs.lstat(requested);if(linkStat.isSymbolicLink())throw Object.assign(new Error("Symbolic links cannot be deleted through delete_file"),{code:"INVALID_TARGET"});if(!linkStat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});await fs.unlink(requested);return{path:requested,deleted:true};}
60
- if(tool==="fs.delete_empty_directory"){const requested=path.resolve(String(args.path||""));const target=security.resolveFilesystemPath(requested);security.resolveFilesystemPath(path.dirname(requested));const linkStat=await fs.lstat(requested);if(linkStat.isSymbolicLink())throw Object.assign(new Error("Symbolic links cannot be deleted through delete_empty_directory"),{code:"INVALID_TARGET"});if(!linkStat.isDirectory())throw Object.assign(new Error("Target must be a directory"),{code:"INVALID_TARGET"});const entries=await fs.readdir(requested);if(entries.length)throw Object.assign(new Error("Directory must be empty"),{code:"DIRECTORY_NOT_EMPTY"});await fs.rmdir(requested);return{path:target,deleted:true};}
61
- if(tool==="fs.write_text"){
62
- const target=security.resolveFilesystemPath(args.path),content=String(args.content??""),expectedSha256=String(args.expectedSha256||"").toLowerCase();
63
- const bytes=Buffer.byteLength(content,"utf8");if(bytes>262144)throw Object.assign(new Error("content exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
64
- if(!/^[a-f0-9]{64}$/.test(expectedSha256))throw Object.assign(new Error("expectedSha256 is required"),{code:"INVALID_ARGUMENT"});
65
- const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});if(stat.size>262144)throw Object.assign(new Error("Target exceeds 262144 bytes"),{code:"CONTENT_TOO_LARGE"});
66
- const current=await fs.readFile(target);const actualSha256=crypto.createHash("sha256").update(current).digest("hex");if(actualSha256!==expectedSha256)throw Object.assign(new Error("File changed since it was read"),{code:"FILE_CHANGED"});
67
- const beforeWrite=await fs.readFile(target);const beforeWriteSha256=crypto.createHash("sha256").update(beforeWrite).digest("hex");if(beforeWriteSha256!==expectedSha256)throw Object.assign(new Error("File changed before write"),{code:"FILE_CHANGED"});
68
- await fs.writeFile(target,content,{encoding:"utf8",flag:"w"});return{path:target,bytes,sha256:crypto.createHash("sha256").update(content,"utf8").digest("hex")};
69
- }
70
- if(tool==="fs.create_text"){
71
- const target=security.resolveFilesystemPath(args.path);
72
- const content=String(args.content??"");
73
- const bytes=Buffer.byteLength(content,"utf8");
74
- if(bytes>262144)throw Object.assign(new Error("content exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
75
- try{await fs.writeFile(target,content,{encoding:"utf8",flag:"wx"});}
76
- catch(error){if(error?.code==="EEXIST")throw Object.assign(new Error("Target already exists"),{code:"FILE_EXISTS"});throw error;}
77
- return{path:target,bytes};
78
- }
79
- if(tool==="fs.replace_text"){
80
- const target=security.resolveFilesystemPath(args.path);
81
- const expected=String(args.expected??"");
82
- const replacement=String(args.replacement??"");
83
- if(!expected)throw Object.assign(new Error("expected text is required"),{code:"INVALID_ARGUMENT"});
84
- if(Buffer.byteLength(expected,"utf8")>262144||Buffer.byteLength(replacement,"utf8")>262144)throw Object.assign(new Error("text exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
85
- const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});
86
- if(stat.size>262144)throw Object.assign(new Error("Target exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
87
- const content=await fs.readFile(target,"utf8");
88
- const first=content.indexOf(expected);if(first<0)throw Object.assign(new Error("Expected text was not found"),{code:"TEXT_NOT_FOUND"});
89
- if(content.indexOf(expected,first+expected.length)>=0)throw Object.assign(new Error("Expected text is not unique"),{code:"TEXT_NOT_UNIQUE"});
90
- const updated=content.slice(0,first)+replacement+content.slice(first+expected.length);
91
- const bytes=Buffer.byteLength(updated,"utf8");if(bytes>262144)throw Object.assign(new Error("Updated content exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
92
- await fs.writeFile(target,updated,{encoding:"utf8",flag:"w"});
93
- return{path:target,bytes,replacements:1};
94
- }
95
- if(tool==="deploy.prepare_local"){if(!deploymentManager)throw Object.assign(new Error("Deployment is not configured"),{code:"DEPLOY_PROFILE_DENIED"});return deploymentManager.prepareLocal({profile:args.profile,commit:args.commit,baseCommit:args.baseCommit??null});}
96
- if(tool==="deploy.prepare_release"){
97
- if(!deploymentManager)throw Object.assign(new Error("Deployment is not configured"),{code:"DEPLOY_PROFILE_DENIED"});
98
- if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});
99
- const local=await deploymentManager.prepareLocal({profile:args.profile,commit:args.commit,baseCommit:args.baseCommit??null});
100
- const staged=await deploymentManager.stageArtifact({profile:args.profile,commit:local.commit,artifact:local.artifact,artifactSha256:local.artifactSha256});
101
- const ownerId=deploymentOwner(args);if(!ownerId)throw Object.assign(new Error("Deployment owner is required"),{code:"DEPLOY_OWNER_REQUIRED"});
102
- const verify=remoteExecutionManager.startDeployHelper({alias:staged.remoteAlias,input:{operation:"verify",commit:local.commit,artifactSha256:local.artifactSha256},timeoutMs:args.timeoutMs,ownerId});
103
- prunePendingDeployments();pendingDeployments.set(verify.id,{ownerId,remoteAlias:staged.remoteAlias,commit:local.commit,artifactSha256:local.artifactSha256,createdAt:Date.now()});
104
- return{profile:local.profile,commit:local.commit,shortSha:local.shortSha,artifactSha256:local.artifactSha256,dependencyChanged:local.dependencyChanged,artifactPath:staged.artifactPath,verifyJobId:verify.id};
105
- }
106
- if(tool==="deploy.continue_prepare"){
107
- if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});
108
- prunePendingDeployments();const state=pendingDeployments.get(args.verifyJobId);const ownerId=deploymentOwner(args);
109
- if(!state||!ownerId||state.ownerId!==ownerId)throw Object.assign(new Error("Deployment prepare state is unavailable"),{code:"DEPLOY_NOT_PREPARED"});
110
- const verified=remoteExecutionManager.output({id:args.verifyJobId,ownerId});
111
- if(verified.running)throw Object.assign(new Error("Deployment verification is still running"),{code:"DEPLOY_VERIFY_PENDING"});
112
- if(verified.exitCode!==0||verified.errorCode){pendingDeployments.delete(args.verifyJobId);throw Object.assign(new Error("Deployment verification failed"),{code:"DEPLOY_VERIFY_FAILED"});}
113
- pendingDeployments.delete(args.verifyJobId);
114
- const prepared=remoteExecutionManager.startDeployHelper({alias:state.remoteAlias,input:{operation:"prepare",commit:state.commit,artifactSha256:state.artifactSha256},timeoutMs:args.timeoutMs,ownerId});
115
- return{prepareJobId:prepared.id,remoteAlias:prepared.alias};
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()};}
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);}
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});}
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);}
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);}
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);}
124
- if(tool==="process.discover")return processManager.discover(args);
125
- if(tool==="process.start"){const cwd=security.resolveFilesystemPath(args.cwd);return processManager.start({...args,cwd});}
126
- if(tool==="process.output")return processManager.output(args);
127
- if(tool==="process.input")return processManager.input(args);
128
- if(tool==="process.stop")return processManager.stop(args);
129
- 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};}
130
- if(tool==="git.stage_paths"||tool==="git.commit"||tool==="git.push"){
131
- const cwd=security.resolveFilesystemPath(args.cwd);
132
- const {stdout:rootRaw}=await execFileAsync("git",["rev-parse","--show-toplevel"],{cwd,windowsHide:true,maxBuffer:1024*1024});
133
- const repoRoot=security.resolveFilesystemPath(rootRaw.trim());
134
- if(tool==="git.stage_paths"){
135
- const {stdout:alreadyStaged}=await execFileAsync("git",["diff","--cached","--name-only"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
136
- if(alreadyStaged.trim())throw Object.assign(new Error("Repository already has staged changes"),{code:"INDEX_NOT_EMPTY"});
137
- const paths=Array.isArray(args.paths)?args.paths.map(String):[];
138
- if(paths.length<1||paths.length>64)throw Object.assign(new Error("paths must contain 1 to 64 explicit paths"),{code:"INVALID_ARGUMENT"});
139
- const relativePaths=paths.map((item)=>{
140
- if(!item||item==="."||item==="*"||item.startsWith("-"))throw Object.assign(new Error("Only explicit repository paths may be staged"),{code:"INVALID_ARGUMENT"});
141
- const absolute=security.resolveFilesystemPath(path.resolve(repoRoot,item));
142
- const relative=path.relative(repoRoot,absolute);
143
- if(!relative||relative.startsWith(".."+path.sep)||path.isAbsolute(relative))throw Object.assign(new Error("Path outside repository"),{code:"PATH_DENIED"});
144
- return relative.split(path.sep).join("/");
145
- });
146
- await execFileAsync("git",["add","--",...relativePaths],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
147
- const {stdout}=await execFileAsync("git",["diff","--cached","--name-only","--",...relativePaths],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
148
- return{staged:stdout.split(/\r?\n/).filter(Boolean)};
149
- }
150
- if(tool==="git.push"){
151
- const {stdout,stderr}=await execFileAsync("git",["push"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
152
- const {stdout:sha}=await execFileAsync("git",["rev-parse","HEAD"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
153
- return{sha:sha.trim(),stdout,stderr};
154
- }
155
- const message=String(args.message||"");
156
- if(!message.trim()||message.length>500||/[\r\n]/.test(message))throw Object.assign(new Error("Commit message must be 1 to 500 characters on one line"),{code:"INVALID_ARGUMENT"});
157
- const {stdout:before}=await execFileAsync("git",["diff","--cached","--name-only"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
158
- const committedFiles=before.split(/\r?\n/).filter(Boolean);
159
- if(!committedFiles.length)throw Object.assign(new Error("No staged changes to commit"),{code:"NOTHING_STAGED"});
160
- await execFileAsync("git",["commit","-m",message],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
161
- const {stdout:sha}=await execFileAsync("git",["rev-parse","HEAD"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
162
- return{sha:sha.trim(),committedFiles};
163
- }
164
- throw Object.assign(new Error("Unsupported tool"),{code:"TOOL_NOT_FOUND"});
165
- };
166
- }
19
+ if(tool==="document.capabilities")return precisionDocumentManager?.capabilities()??{available:false};
20
+ if(tool==="document.inspect"){if(!precisionDocumentManager)throw Object.assign(new Error("Precision document manager is not configured"),{code:"DOCUMENT_NOT_AVAILABLE"});return precisionDocumentManager.inspect(args);}
21
+ if(tool==="document.compare"){if(!precisionDocumentManager)throw Object.assign(new Error("Precision document manager is not configured"),{code:"DOCUMENT_NOT_AVAILABLE"});return precisionDocumentManager.compare(args);}
22
+ if(tool==="document.mutate_docx"){if(!precisionDocumentManager)throw Object.assign(new Error("Precision document manager is not configured"),{code:"DOCUMENT_NOT_AVAILABLE"});return precisionDocumentManager.mutateDocx(args);}
23
+ if(tool==="document.mutate_xlsx"){if(!precisionDocumentManager)throw Object.assign(new Error("Precision document manager is not configured"),{code:"DOCUMENT_NOT_AVAILABLE"});return precisionDocumentManager.mutateXlsx(args);}
24
+ if(tool==="document.mutate_pptx"){if(!precisionDocumentManager)throw Object.assign(new Error("Precision document manager is not configured"),{code:"DOCUMENT_NOT_AVAILABLE"});return precisionDocumentManager.mutatePptx(args);}
25
+ if(tool==="gui.capabilities")return guiManager?.capabilities()??{available:false,backend:null,observation:{displays:false,windows:false,foreground:false,capture:false},interaction:false,accessibility:false};
26
+ if(tool==="gui.displays")return guiManager.displays();
27
+ if(tool==="gui.windows")return guiManager.windows();
28
+ if(tool==="gui.foreground")return guiManager.foreground();
29
+ if(tool==="gui.capture")return guiManager.capture(args);
30
+ 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);}
31
+ if(tool==="fs.begin_binary_write")return binaryTransferManager.begin(args);
32
+ if(tool==="fs.write_binary_session")return binaryTransferManager.write(args);
33
+ if(tool==="fs.finalize_binary_write")return binaryTransferManager.finalize(args);
34
+ if(tool==="fs.abort_binary_write")return binaryTransferManager.abort(args);
35
+ if(tool==="fs.list"){const target=security.resolveFilesystemPath(args.path);const entries=await fs.readdir(target,{withFileTypes:true});return{path:target,entries:entries.map(e=>({name:e.name,type:e.isDirectory()?"directory":e.isFile()?"file":"other"}))};}
36
+ if(tool==="fs.read"){const target=security.resolveFilesystemPath(args.path);const max=Math.min(Number(args.maxBytes||65536),262144);const h=await fs.open(target,"r");try{const stat=await h.stat();if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});const readBytes=Math.min(stat.size,262144);const snapshot=Buffer.alloc(readBytes);const {bytesRead}=await h.read(snapshot,0,readBytes,0);const data=snapshot.subarray(0,bytesRead);return{path:target,content:data.subarray(0,max).toString("utf8"),truncated:stat.size>max,size:stat.size,...(stat.size<=262144&&bytesRead===stat.size?{sha256:crypto.createHash("sha256").update(data).digest("hex")}:{})};}finally{await h.close();}}
37
+ if(tool==="fs.search"){
38
+ const root=security.resolveFilesystemPath(args.path);const q=String(args.query||"");if(!q)throw Object.assign(new Error("query is required"),{code:"INVALID_ARGUMENT"});
39
+ const mode=args.mode==="content"?"content":"name",maxResults=Math.min(Math.max(Number(args.maxResults||50),1),200),maxDepth=Math.min(Math.max(Number(args.maxDepth??8),0),20),maxFileBytes=Math.min(Math.max(Number(args.maxFileBytes||262144),1),1048576),matches=[];
40
+ const needle=q.toLowerCase();
41
+ async function walk(dir,depth){
42
+ if(matches.length>=maxResults||depth>maxDepth)return;
43
+ const entries=await fs.readdir(dir,{withFileTypes:true});
44
+ for(const entry of entries){
45
+ if(matches.length>=maxResults)break;
46
+ const candidate=path.join(dir,entry.name);let target;try{target=security.resolveFilesystemPath(candidate);}catch{continue;}
47
+ if(entry.isDirectory()){if(mode==="name"&&entry.name.toLowerCase().includes(needle))matches.push({name:entry.name,path:target,type:"directory"});await walk(target,depth+1);continue;}
48
+ if(!entry.isFile())continue;
49
+ if(mode==="name"){if(entry.name.toLowerCase().includes(needle))matches.push({name:entry.name,path:target,type:"file"});continue;}
50
+ const stat=await fs.stat(target);if(stat.size>maxFileBytes)continue;
51
+ const raw=await fs.readFile(target);if(raw.includes(0))continue;const content=raw.toString("utf8");const lines=content.split(/\r?\n/);
52
+ for(let i=0;i<lines.length&&matches.length<maxResults;i++){if(lines[i].toLowerCase().includes(needle))matches.push({name:entry.name,path:target,type:"file",line:i+1,preview:lines[i].slice(0,500)});}
53
+ }
54
+ }
55
+ await walk(root,0);return{path:root,query:q,mode,matches,truncated:matches.length>=maxResults};
56
+ }
57
+ if(tool==="fs.info"){const target=security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);return{path:target,type:stat.isDirectory()?"directory":stat.isFile()?"file":"other",size:stat.size,createdAt:stat.birthtime.toISOString(),modifiedAt:stat.mtime.toISOString()};}
58
+ if(tool==="fs.hash"){const target=security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});const hash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(target);stream.on("data",(chunk)=>hash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});return{path:target,size:stat.size,sha256:hash.digest("hex")};}
59
+ if(tool==="fs.read_binary"){const target=security.resolveFilesystemPath(args.path);const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});const offset=Number(args.offset||0),maxBytes=Number(args.maxBytes||32768);if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(maxBytes)||maxBytes<1||maxBytes>32768||offset>stat.size)throw Object.assign(new Error("Invalid binary range"),{code:"INVALID_ARGUMENT"});const bytesToRead=Math.min(maxBytes,stat.size-offset),handle=await fs.open(target,"r");try{const buffer=Buffer.alloc(bytesToRead),{bytesRead}=await handle.read(buffer,0,bytesToRead,offset);return{path:target,offset,bytesRead,size:stat.size,dataBase64:buffer.subarray(0,bytesRead).toString("base64"),eof:offset+bytesRead>=stat.size};}finally{await handle.close();}}
60
+ if(tool==="fs.create_binary"){const target=security.resolveFilesystemPath(args.path),encoded=String(args.dataBase64??"");if(encoded.length>43692||encoded.length%4!==0||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded))throw Object.assign(new Error("Invalid or oversized binary chunk"),{code:"INVALID_ARGUMENT"});const data=Buffer.from(encoded,"base64");if(data.length>32768)throw Object.assign(new Error("Binary chunk exceeds 32768 bytes"),{code:"CONTENT_TOO_LARGE"});try{await fs.writeFile(target,data,{flag:"wx"});}catch(error){if(error?.code==="EEXIST")throw Object.assign(new Error("Target already exists"),{code:"FILE_EXISTS"});throw error;}return{path:target,size:data.length,sha256:crypto.createHash("sha256").update(data).digest("hex")};}
61
+ if(tool==="fs.write_binary"){const target=security.resolveFilesystemPath(args.path),offset=Number(args.offset),expectedSize=Number(args.expectedSize),expectedSha256=String(args.expectedSha256||"").toLowerCase(),encoded=String(args.dataBase64??"");if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(expectedSize)||expectedSize<0||offset!==expectedSize||!/^[a-f0-9]{64}$/.test(expectedSha256))throw Object.assign(new Error("Invalid guarded binary write"),{code:"INVALID_ARGUMENT"});if(encoded.length>43692||encoded.length%4!==0||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded))throw Object.assign(new Error("Invalid or oversized binary chunk"),{code:"INVALID_ARGUMENT"});const data=Buffer.from(encoded,"base64");if(data.length>32768)throw Object.assign(new Error("Binary chunk exceeds 32768 bytes"),{code:"CONTENT_TOO_LARGE"});const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});if(stat.size!==expectedSize)throw Object.assign(new Error("File changed since previous chunk"),{code:"FILE_CHANGED"});const hash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(target);stream.on("data",(chunk)=>hash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});if(hash.digest("hex")!==expectedSha256)throw Object.assign(new Error("File changed since previous chunk"),{code:"FILE_CHANGED"});await fs.appendFile(target,data);const updatedHash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(target);stream.on("data",(chunk)=>updatedHash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});return{path:target,size:expectedSize+data.length,sha256:updatedHash.digest("hex")};}
62
+ if(tool==="fs.copy"){const source=security.resolveFilesystemPath(args.source),destination=security.resolveFilesystemPath(args.destination),stat=await fs.stat(source);if(!stat.isFile())throw Object.assign(new Error("Source must be a regular file"),{code:"INVALID_TARGET"});try{await fs.copyFile(source,destination,fs.constants?.COPYFILE_EXCL??1);}catch(error){if(error?.code==="EEXIST")throw Object.assign(new Error("Destination already exists"),{code:"FILE_EXISTS"});throw error;}const copied=await fs.stat(destination),hash=crypto.createHash("sha256");await new Promise((resolve,reject)=>{const stream=createReadStream(destination);stream.on("data",(chunk)=>hash.update(chunk));stream.on("error",reject);stream.on("end",resolve);});return{source,destination,size:copied.size,sha256:hash.digest("hex")};}
63
+ if(tool==="fs.mkdir"){const target=security.resolveFilesystemPath(args.path);try{const stat=await fs.stat(target);if(!stat.isDirectory())throw Object.assign(new Error("Target exists and is not a directory"),{code:"FILE_EXISTS"});return{path:target,created:false};}catch(error){if(error?.code!=="ENOENT")throw error;}await fs.mkdir(target,{recursive:Boolean(args.recursive)});return{path:target,created:true};}
64
+ if(tool==="fs.move"){const source=security.resolveFilesystemPath(args.source);const destination=security.resolveFilesystemPath(args.destination);try{await fs.lstat(destination);throw Object.assign(new Error("Destination already exists"),{code:"FILE_EXISTS"});}catch(error){if(error?.code!=="ENOENT")throw error;}await fs.rename(source,destination);return{source,destination};}
65
+ if(tool==="fs.delete_file"){const requested=path.resolve(String(args.path||""));const target=security.resolveFilesystemPath(requested);const linkStat=await fs.lstat(requested);if(linkStat.isSymbolicLink())throw Object.assign(new Error("Symbolic links cannot be deleted through delete_file"),{code:"INVALID_TARGET"});if(!linkStat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});await fs.unlink(requested);return{path:requested,deleted:true};}
66
+ if(tool==="fs.delete_empty_directory"){const requested=path.resolve(String(args.path||""));const target=security.resolveFilesystemPath(requested);security.resolveFilesystemPath(path.dirname(requested));const linkStat=await fs.lstat(requested);if(linkStat.isSymbolicLink())throw Object.assign(new Error("Symbolic links cannot be deleted through delete_empty_directory"),{code:"INVALID_TARGET"});if(!linkStat.isDirectory())throw Object.assign(new Error("Target must be a directory"),{code:"INVALID_TARGET"});const entries=await fs.readdir(requested);if(entries.length)throw Object.assign(new Error("Directory must be empty"),{code:"DIRECTORY_NOT_EMPTY"});await fs.rmdir(requested);return{path:target,deleted:true};}
67
+ if(tool==="fs.write_text"){
68
+ const target=security.resolveFilesystemPath(args.path),content=String(args.content??""),expectedSha256=String(args.expectedSha256||"").toLowerCase();
69
+ const bytes=Buffer.byteLength(content,"utf8");if(bytes>262144)throw Object.assign(new Error("content exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
70
+ if(!/^[a-f0-9]{64}$/.test(expectedSha256))throw Object.assign(new Error("expectedSha256 is required"),{code:"INVALID_ARGUMENT"});
71
+ const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});if(stat.size>262144)throw Object.assign(new Error("Target exceeds 262144 bytes"),{code:"CONTENT_TOO_LARGE"});
72
+ const current=await fs.readFile(target);const actualSha256=crypto.createHash("sha256").update(current).digest("hex");if(actualSha256!==expectedSha256)throw Object.assign(new Error("File changed since it was read"),{code:"FILE_CHANGED"});
73
+ const beforeWrite=await fs.readFile(target);const beforeWriteSha256=crypto.createHash("sha256").update(beforeWrite).digest("hex");if(beforeWriteSha256!==expectedSha256)throw Object.assign(new Error("File changed before write"),{code:"FILE_CHANGED"});
74
+ await fs.writeFile(target,content,{encoding:"utf8",flag:"w"});return{path:target,bytes,sha256:crypto.createHash("sha256").update(content,"utf8").digest("hex")};
75
+ }
76
+ if(tool==="fs.create_text"){
77
+ const target=security.resolveFilesystemPath(args.path);
78
+ const content=String(args.content??"");
79
+ const bytes=Buffer.byteLength(content,"utf8");
80
+ if(bytes>262144)throw Object.assign(new Error("content exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
81
+ try{await fs.writeFile(target,content,{encoding:"utf8",flag:"wx"});}
82
+ catch(error){if(error?.code==="EEXIST")throw Object.assign(new Error("Target already exists"),{code:"FILE_EXISTS"});throw error;}
83
+ return{path:target,bytes};
84
+ }
85
+ if(tool==="fs.replace_text"){
86
+ const target=security.resolveFilesystemPath(args.path);
87
+ const expected=String(args.expected??"");
88
+ const replacement=String(args.replacement??"");
89
+ if(!expected)throw Object.assign(new Error("expected text is required"),{code:"INVALID_ARGUMENT"});
90
+ if(Buffer.byteLength(expected,"utf8")>262144||Buffer.byteLength(replacement,"utf8")>262144)throw Object.assign(new Error("text exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
91
+ const stat=await fs.stat(target);if(!stat.isFile())throw Object.assign(new Error("Target must be a regular file"),{code:"INVALID_TARGET"});
92
+ if(stat.size>262144)throw Object.assign(new Error("Target exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
93
+ const content=await fs.readFile(target,"utf8");
94
+ const first=content.indexOf(expected);if(first<0)throw Object.assign(new Error("Expected text was not found"),{code:"TEXT_NOT_FOUND"});
95
+ if(content.indexOf(expected,first+expected.length)>=0)throw Object.assign(new Error("Expected text is not unique"),{code:"TEXT_NOT_UNIQUE"});
96
+ const updated=content.slice(0,first)+replacement+content.slice(first+expected.length);
97
+ const bytes=Buffer.byteLength(updated,"utf8");if(bytes>262144)throw Object.assign(new Error("Updated content exceeds 262144 UTF-8 bytes"),{code:"CONTENT_TOO_LARGE"});
98
+ await fs.writeFile(target,updated,{encoding:"utf8",flag:"w"});
99
+ return{path:target,bytes,replacements:1};
100
+ }
101
+ if(tool==="deploy.prepare_local"){if(!deploymentManager)throw Object.assign(new Error("Deployment is not configured"),{code:"DEPLOY_PROFILE_DENIED"});return deploymentManager.prepareLocal({profile:args.profile,commit:args.commit,baseCommit:args.baseCommit??null});}
102
+ if(tool==="deploy.prepare_release"){
103
+ if(!deploymentManager)throw Object.assign(new Error("Deployment is not configured"),{code:"DEPLOY_PROFILE_DENIED"});
104
+ if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});
105
+ const local=await deploymentManager.prepareLocal({profile:args.profile,commit:args.commit,baseCommit:args.baseCommit??null});
106
+ const staged=await deploymentManager.stageArtifact({profile:args.profile,commit:local.commit,artifact:local.artifact,artifactSha256:local.artifactSha256});
107
+ const ownerId=deploymentOwner(args);if(!ownerId)throw Object.assign(new Error("Deployment owner is required"),{code:"DEPLOY_OWNER_REQUIRED"});
108
+ const verify=remoteExecutionManager.startDeployHelper({alias:staged.remoteAlias,input:{operation:"verify",commit:local.commit,artifactSha256:local.artifactSha256},timeoutMs:args.timeoutMs,ownerId});
109
+ prunePendingDeployments();pendingDeployments.set(verify.id,{ownerId,remoteAlias:staged.remoteAlias,commit:local.commit,artifactSha256:local.artifactSha256,createdAt:Date.now()});
110
+ return{profile:local.profile,commit:local.commit,shortSha:local.shortSha,artifactSha256:local.artifactSha256,dependencyChanged:local.dependencyChanged,artifactPath:staged.artifactPath,verifyJobId:verify.id};
111
+ }
112
+ if(tool==="deploy.continue_prepare"){
113
+ if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});
114
+ prunePendingDeployments();const state=pendingDeployments.get(args.verifyJobId);const ownerId=deploymentOwner(args);
115
+ if(!state||!ownerId||state.ownerId!==ownerId)throw Object.assign(new Error("Deployment prepare state is unavailable"),{code:"DEPLOY_NOT_PREPARED"});
116
+ const verified=remoteExecutionManager.output({id:args.verifyJobId,ownerId});
117
+ if(verified.running)throw Object.assign(new Error("Deployment verification is still running"),{code:"DEPLOY_VERIFY_PENDING"});
118
+ if(verified.exitCode!==0||verified.errorCode){pendingDeployments.delete(args.verifyJobId);throw Object.assign(new Error("Deployment verification failed"),{code:"DEPLOY_VERIFY_FAILED"});}
119
+ pendingDeployments.delete(args.verifyJobId);
120
+ const prepared=remoteExecutionManager.startDeployHelper({alias:state.remoteAlias,input:{operation:"prepare",commit:state.commit,artifactSha256:state.artifactSha256},timeoutMs:args.timeoutMs,ownerId});
121
+ return{prepareJobId:prepared.id,remoteAlias:prepared.alias};
122
+ }
123
+ if(tool==="remote.targets"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return{targets:remoteExecutionManager.listTargets()};}
124
+ if(tool==="remote.start"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return remoteExecutionManager.start(args);}
125
+ 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});}
126
+ 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});}
127
+ 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);}
128
+ 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);}
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
+ if(tool==="process.discover")return processManager.discover(args);
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);
133
+ if(tool==="process.input")return processManager.input(args);
134
+ if(tool==="process.stop")return processManager.stop(args);
135
+ 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};}
136
+ if(tool==="git.stage_paths"||tool==="git.commit"||tool==="git.push"){
137
+ const cwd=security.resolveFilesystemPath(args.cwd);
138
+ const {stdout:rootRaw}=await execFileAsync("git",["rev-parse","--show-toplevel"],{cwd,windowsHide:true,maxBuffer:1024*1024});
139
+ const repoRoot=security.resolveFilesystemPath(rootRaw.trim());
140
+ if(tool==="git.stage_paths"){
141
+ const {stdout:alreadyStaged}=await execFileAsync("git",["diff","--cached","--name-only"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
142
+ if(alreadyStaged.trim())throw Object.assign(new Error("Repository already has staged changes"),{code:"INDEX_NOT_EMPTY"});
143
+ const paths=Array.isArray(args.paths)?args.paths.map(String):[];
144
+ if(paths.length<1||paths.length>64)throw Object.assign(new Error("paths must contain 1 to 64 explicit paths"),{code:"INVALID_ARGUMENT"});
145
+ const relativePaths=paths.map((item)=>{
146
+ if(!item||item==="."||item==="*"||item.startsWith("-"))throw Object.assign(new Error("Only explicit repository paths may be staged"),{code:"INVALID_ARGUMENT"});
147
+ const absolute=security.resolveFilesystemPath(path.resolve(repoRoot,item));
148
+ const relative=path.relative(repoRoot,absolute);
149
+ if(!relative||relative.startsWith(".."+path.sep)||path.isAbsolute(relative))throw Object.assign(new Error("Path outside repository"),{code:"PATH_DENIED"});
150
+ return relative.split(path.sep).join("/");
151
+ });
152
+ await execFileAsync("git",["add","--",...relativePaths],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
153
+ const {stdout}=await execFileAsync("git",["diff","--cached","--name-only","--",...relativePaths],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
154
+ return{staged:stdout.split(/\r?\n/).filter(Boolean)};
155
+ }
156
+ if(tool==="git.push"){
157
+ const {stdout,stderr}=await execFileAsync("git",["push"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
158
+ const {stdout:sha}=await execFileAsync("git",["rev-parse","HEAD"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
159
+ return{sha:sha.trim(),stdout,stderr};
160
+ }
161
+ const message=String(args.message||"");
162
+ if(!message.trim()||message.length>500||/[\r\n]/.test(message))throw Object.assign(new Error("Commit message must be 1 to 500 characters on one line"),{code:"INVALID_ARGUMENT"});
163
+ const {stdout:before}=await execFileAsync("git",["diff","--cached","--name-only"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
164
+ const committedFiles=before.split(/\r?\n/).filter(Boolean);
165
+ if(!committedFiles.length)throw Object.assign(new Error("No staged changes to commit"),{code:"NOTHING_STAGED"});
166
+ await execFileAsync("git",["commit","-m",message],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
167
+ const {stdout:sha}=await execFileAsync("git",["rev-parse","HEAD"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
168
+ return{sha:sha.trim(),committedFiles};
169
+ }
170
+ throw Object.assign(new Error("Unsupported tool"),{code:"TOOL_NOT_FOUND"});
171
+ };
172
+ }