chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

@@ -0,0 +1,98 @@
import { createHash } from 'node:crypto';
import { authorizedElectronFile } from './ElectronFileScope.js';
import { decodeCanonicalBase64 } from '../domain/webScreenshot.js';
const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024;
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
const types=new Set(['image/png','image/jpeg','image/gif','image/webp']);
const mime=value=>value==='image/jpg'?'image/jpeg':value;
function sniff(bytes){
if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png';
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif';
if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp';
}
function safeSourceUrl(value){
try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;}
}
/** Only a caller-selected IMG and its already loaded main-frame resource are eligible. */
export async function captureElectronMediaAssets(snapshot,frames,selectors,send){
if(!selectors?.length)return undefined;
const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[
'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.',
'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.',
'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.'
]};
const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
const document=snapshot.documents?.find(value=>str(value.frameId)===main.frame_id);
const stable=async()=>{const frame=(await send('Page.getFrameTree',{})).frameTree?.frame;if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw Error('frame_changed');};
for(const selector of selectors.slice(0,MAX_ASSETS)){
const item={selector,frame_id:main.frame_id,status:'unknown',reason:'unavailable'};
result.items.push(item);
try{
await stable();
if(!document||await authorizedElectronFile(str(document.documentURL))!==main.file_path)throw Error('snapshot_frame_unavailable');
const root=(await send('DOM.getDocument',{depth:0,pierce:false})).root;
const ids=(await send('DOM.querySelectorAll',{nodeId:root.nodeId,selector})).nodeIds??[];
if(ids.length!==1)throw Error(ids.length?'selector_ambiguous':'selector_absent');
const node=(await send('DOM.describeNode',{nodeId:ids[0],depth:0,pierce:false})).node;
if(node?.nodeName!=='IMG')throw Error('selected_node_not_img');
const raw=document.nodes??{},index=raw.backendNodeId?.indexOf(node.backendNodeId)??-1;
if(index<0||str(raw.nodeName?.[index])!=='IMG')throw Error('snapshot_identity_unavailable');
item.node_index=index;item.backend_node_id=node.backendNodeId;
let excluded=false;
for(let n=index;n>=0;n=raw.parentIndex?.[n]??-1){const tag=str(raw.nodeName?.[n]),attrs=raw.attributes?.[n]??[];if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag))excluded=true;for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='contenteditable'&&str(attrs[a+1])!=='false')excluded=true;}
if(excluded)throw Error('editable_subtree_excluded');
const layoutIndex=document.layout?.nodeIndex?.indexOf(index)??-1,bounds=document.layout?.bounds?.[layoutIndex];
item.visibility={status:'unknown',reason:'no_visible_layout'};
if(layoutIndex>=0&&bounds&&bounds[2]>0&&bounds[3]>0)try{
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
const x=bounds[0]-(viewport.pageX??document.scrollOffsetX??0),y=bounds[1]-(viewport.pageY??document.scrollOffsetY??0);
const left=Math.max(0,x),top=Math.max(0,y),right=Math.min(viewport.clientWidth,x+bounds[2]),bottom=Math.min(viewport.clientHeight,y+bounds[3]);
if(right<=left||bottom<=top)item.visibility={status:'unknown',reason:'outside_visible_viewport'};
else{
item.visibility={status:'covered',reason:'sampled_points_hit_other_layers'};item.visibility_samples=[];
for(const [fx,fy] of [[.5,.05],[.5,.5],[.5,.95],[.05,.05],[.95,.05],[.05,.5],[.95,.5],[.05,.95],[.95,.95]]){
const point={x:Math.floor(left+(right-left)*fx),y:Math.floor(top+(bottom-top)*fy)};
const hit=await send('DOM.getNodeForLocation',{...point,includeUserAgentShadowDOM:false,ignorePointerEventsNone:true});
const hitIndex=raw.backendNodeId?.indexOf(hit.backendNodeId)??-1,hitAttrs=raw.attributes?.[hitIndex]??[];
let hitClass='';for(let a=0;a<hitAttrs.length;a+=2)if(str(hitAttrs[a])==='class')hitClass=str(hitAttrs[a+1]).slice(0,500);
item.visibility_samples.push({...point,...(hit.backendNodeId?{hit_backend_node_id:hit.backendNodeId}:{}),...(hitIndex>=0?{hit_node_index:hitIndex,hit_node_name:str(raw.nodeName?.[hitIndex]),hit_class:hitClass}:{})});
if(hit.backendNodeId===node.backendNodeId){item.visibility={status:'observed'};break;}
}
}
}catch{item.visibility={status:'unknown',reason:'hit_test_unavailable'};}
const current=raw.currentSourceURL,at=current?.index?.indexOf(index)??-1;
let source=at>=0?str(current.value?.[at]):'',selection='current_source_url';
if(!source){selection='src';const attrs=raw.attributes?.[index]??[];for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='src')source=str(attrs[a+1]);}
if(!source)throw Error('source_url_unavailable');
const selectedUrl=new URL(source,str(document.documentURL)),url=new URL(selectedUrl);url.hash='';
const selectedAttributes={},nodeAttrs=raw.attributes?.[index]??[];
for(let a=0;a<nodeAttrs.length;a+=2){const name=str(nodeAttrs[a]),value=str(nodeAttrs[a+1]);if(['class','id','width','height','loading','decoding','alt'].includes(name))selectedAttributes[name]=value.slice(0,500);else if(name==='src'){try{selectedAttributes.src=safeSourceUrl(new URL(value,str(document.documentURL)).href)??'[URL omitted]';}catch{selectedAttributes.src='[URL unavailable]';}}}
item.selected_node={node_name:'IMG',attributes:selectedAttributes,current_source_url:at>=0?safeSourceUrl(str(current.value?.[at])):null};
const tree=(await send('Page.getResourceTree',{})).frameTree;
if(!tree?.frame||tree.frame.id!==main.frame_id||await authorizedElectronFile(tree.frame.url)!==main.file_path)throw Error('frame_changed');
const resource=tree.resources?.find(value=>value.url===url.href&&value.type==='Image'&&!value.failed&&!value.canceled);
if(!resource)throw Error('selected_source_not_loaded_in_main_frame');
const contentType=mime(resource.mimeType?.toLowerCase());
item.source={url:safeSourceUrl(selectedUrl.href),url_sha256:sha(selectedUrl.href),selection,mime_type:contentType??'unknown'};
if(!types.has(contentType))throw Error('unsupported_image_type');
if(resource.contentSize>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
const content=await send('Page.getResourceContent',{frameId:main.frame_id,url:resource.url});
if(typeof content.content!=='string'||!content.base64Encoded)throw Error('binary_content_unavailable');
if(content.content.length>4*Math.ceil(MAX_ASSET_BYTES/3))throw Error('asset_byte_budget_exceeded');
const bytes=decodeCanonicalBase64(content.content);
if(!bytes?.length)throw Error('invalid_base64');
if(bytes.length>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
if(result.total_bytes+bytes.length>MAX_TOTAL_BYTES)throw Error('total_byte_budget_exceeded');
if(sniff(bytes)!==contentType)throw Error('image_signature_mismatch');
await stable();
item.status='exported';delete item.reason;
item.artifact={mime_type:contentType,bytes:bytes.length,sha256:sha(bytes),data_base64:bytes.toString('base64')};
result.total_bytes+=bytes.length;
}catch(error){const allowed=new Set(['frame_changed','snapshot_frame_unavailable','selector_ambiguous','selector_absent','selected_node_not_img','snapshot_identity_unavailable','editable_subtree_excluded','no_visible_layout','outside_visible_viewport','visibility_unconfirmed_or_covered','source_url_unavailable','selected_source_not_loaded_in_main_frame','unsupported_image_type','asset_byte_budget_exceeded','binary_content_unavailable','invalid_base64','total_byte_budget_exceeded','image_signature_mismatch']);item.reason=allowed.has(error.message)?error.message:'protocol_resource_unavailable';}
}
return result;
}
@@ -0,0 +1,67 @@
// Read actual fonts used for bounded, visible message/name/code samples. No renderer evaluation.
export async function captureElectronPlatformFonts(snapshot,frames,send) {
const categories=['assistant-name','chinese-body','english-body','user-bubble','code'];
const samples=[],missing=[];
const document=snapshot.documents?.find(d=>snapshot.strings[d.frameId]===frames[0].frame_id);
if(!document)return {samples,missing:categories.map(category=>({category,reason:'Authorized main document absent'}))};
const strings=snapshot.strings,nodes=document.nodes,layout=document.layout;
const value=index=>strings[index]??'';
const className=index=>{const a=nodes.attributes[index]??[];for(let i=0;i<a.length;i+=2)if(value(a[i])==='class')return value(a[i+1]);return ''};
const hasClass=(index,name)=>className(index).split(' ').includes(name);
const under=(index,test)=>{for(let n=index;n>=0;n=nodes.parentIndex[n])if(test(n))return true;return false};
const code=index=>under(index,n=>/^(CODE|PRE)$/.test(value(nodes.nodeName[n])));
const text=index=>{let out='';for(let n=0;n<nodes.nodeName.length;n++)if(value(nodes.nodeName[n])==='#text'&&under(n,i=>i===index))out+=value(nodes.nodeValue[n]);return out};
const viewport=snapshot.reaViewport;
const positionOf=new Map(layout.nodeIndex.map((n,p)=>[n,p]));
const visible=new Set(layout.nodeIndex.filter((_,position)=>{
if(!viewport)return true;
const [x,y,w,h]=layout.bounds[position]??[];
let left=viewport.page_x,top=viewport.page_y,right=left+viewport.width,bottom=top+viewport.height;
for(let n=nodes.parentIndex[layout.nodeIndex[position]];n>=0;n=nodes.parentIndex[n]){
const parentPosition=positionOf.get(n);if(parentPosition===undefined)continue;
const overflowIndex=snapshot.reaLayoutProperties.indexOf('overflow');
const overflow=value((layout.styles[parentPosition]??[])[overflowIndex]);
if(!/auto|scroll|hidden|clip/.test(overflow))continue;
const [px,py,pw,ph]=layout.bounds[parentPosition];
left=Math.max(left,px);top=Math.max(top,py);right=Math.min(right,px+pw);bottom=Math.min(bottom,py+ph);
}
return w>0&&h>0&&x>=left&&y>=top&&x+w<=right&&y+h<=bottom;
}));
const candidates=[];
for(let n=0;n<nodes.nodeName.length;n++)if(value(nodes.nodeName[n])==='#text'){
const parent=nodes.parentIndex[n];if(parent>=0&&visible.has(parent)&&!candidates.includes(parent))candidates.push(parent);
}
await send('DOM.getDocument',{depth:0});
for(const category of categories){
let found=false,reason='No short visible sample of this category in the current document';
const selected=category==='assistant-name'?layout.nodeIndex.filter(n=>visible.has(n)&&hasClass(n,'cr-agent__name')):candidates;
for(const index of selected){
if(under(index,n=>/^(INPUT|TEXTAREA|SELECT)$/.test(value(nodes.nodeName[n]))||(nodes.attributes[n]??[]).some((a,i,attrs)=>i%2===0&&value(a)==='contenteditable'&&value(attrs[i+1])!=='false')))continue;
const sample=text(index);if(!sample.trim()||sample.length>256)continue;
const body=under(index,n=>hasClass(n,'cr-agent__content'))&&under(index,n=>hasClass(n,'cr-text-block'))&&!under(index,n=>hasClass(n,'cr-code-like-box'));
// Plain leaf prose avoids treating a composite bold/inline subtree as one default-body font sample.
const proseLeaf=!nodes.parentIndex.some((parent,n)=>parent===index&&value(nodes.nodeName[n])!=='#text'&&value(nodes.nodeName[n])!=='BR');
if((category==='chinese-body'||category==='english-body')&&!proseLeaf)continue;
const weightProperty=snapshot.reaLayoutProperties.indexOf('font-weight');
if((category==='chinese-body'||category==='english-body')&&weightProperty>=0&&value((layout.styles[positionOf.get(index)]??[])[weightProperty])!=='400')continue;
if(category==='chinese-body'&&(!body||code(index)||!/[\u3400-\u9fff]/.test(sample)))continue;
if(category==='english-body'&&(!body||code(index)||/[\u3400-\u9fff]/.test(sample)||!/[A-Za-z]{3}/.test(sample)))continue;
if(category==='user-bubble'&&!under(index,n=>hasClass(n,'cr-self-bubble')))continue;
if(category==='code'&&!code(index))continue;
const backend=nodes.backendNodeId?.[index];if(!backend){reason='Backend node identity absent';continue;}
try{
const pushed=await send('DOM.pushNodesByBackendIdsToFrontend',{backendNodeIds:[backend]});const nodeId=pushed.nodeIds?.[0];if(!nodeId)continue;
const fonts=(await send('CSS.getPlatformFontsForNode',{nodeId})).fonts;
if(!Array.isArray(fonts)||!fonts.some(f=>f.glyphCount>0)){reason='No rendered glyph fonts reported';continue;}
const position=layout.nodeIndex.indexOf(index),styleIndexes=layout.styles[position]??[];
// The order is supplied by the visuals module, not inferred from font names.
const properties=snapshot.reaLayoutProperties;
const styles=Object.fromEntries(properties.map((p,i)=>[p,value(styleIndexes[i])]));
samples.push({category,text:sample,fontFamily:styles['font-family']??'',...(category==='code'?{codeContext:under(index,n=>value(nodes.nodeName[n])==='PRE')?'block':'inline'}:{}),platformFonts:fonts.map(f=>({familyName:f.familyName,postScriptName:f.postScriptName??'',isCustomFont:f.isCustomFont,glyphCount:f.glyphCount})),bounds:layout.bounds[position],styles});
found=true;break;
}catch{reason='Platform font query failed for the visible candidate';}
}
if(!found)missing.push({category,reason});
}
return {samples,missing};
}
+175
View File
@@ -0,0 +1,175 @@
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const MAX_SHEETS = 1024;
const MAX_SHEET_BYTES = 8 * 1024 * 1024;
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
/** Retain only stylesheet lifecycle metadata from the selected target session. */
export const ingestElectronStyleEvent = (event, stylesMap) => {
if (event?.method === "CSS.styleSheetRemoved") {
const styleSheetId = stringValue(recordValue(event.params)?.styleSheetId);
if (styleSheetId !== undefined)
stylesMap.delete(styleSheetId);
return;
}
if (event?.method !== "CSS.styleSheetAdded")
return;
const header = recordValue(recordValue(event.params)?.header);
const styleSheetId = stringValue(header?.styleSheetId);
if (styleSheetId === undefined || styleSheetId === "")
return;
stylesMap.set(styleSheetId, {
styleSheetId,
frameId: stringValue(header?.frameId) ?? null,
sourceURL: stringValue(header?.sourceURL) ?? null,
origin: stringValue(header?.origin) ?? null,
isInline: header?.isInline === true,
isConstructed: header?.isConstructed === true,
});
};
/** Capture CSS text only for authorized file frames and explicit source opt-in. */
export const captureElectronStyles = async (input) => {
const styles = [...input.styles.values()];
const items = [];
let excluded = 0;
let sourceFailures = 0;
let truncated = false;
let includedBytes = 0;
let sourceRequests = 0;
const limitationNotes = new Set();
void input.completeness;
limitationNotes.add("Stylesheet inventory includes only CSS.styleSheetAdded events still present at the capture snapshot.");
for (const style of styles) {
const scoped = await authorizedStyle(style, input.frameIds);
if (scoped === undefined) {
excluded += 1;
continue;
}
if (input.request?.include_style_sources !== true)
continue;
const identity = {
style_sheet_id: style.styleSheetId,
frame_id: scoped.frameId,
file_path: scoped.filePath,
origin: style.origin,
is_inline: style.isInline,
is_constructed: style.isConstructed,
};
const item = {
style_sheet_key: `electron_stylesheet_${digestCanonicalValue(identity, "CDP capture")}`,
style_sheet_id: style.styleSheetId,
frame_id: scoped.frameId,
file_path: scoped.filePath,
origin: style.origin,
is_inline: style.isInline,
is_constructed: style.isConstructed,
source: { included: false, reason: "CSS source was not requested" },
};
if (sourceRequests >= MAX_SHEETS) {
sourceFailures += 1;
truncated = true;
item.source = { included: false, reason: "stylesheet source count limit reached" };
limitationNotes.add(`CSS source capture reached the ${String(MAX_SHEETS)} stylesheet limit.`);
items.push(item);
continue;
}
sourceRequests += 1;
let sourceText;
try {
const result = await input.connection.send(
"CSS.getStyleSheetText",
{ styleSheetId: style.styleSheetId },
input.sessionId,
input.signal
);
sourceText = stringValue(recordValue(result)?.text);
}
catch (cause) {
if (input.signal?.aborted === true)
throw cause;
sourceFailures += 1;
item.source = { included: false, reason: "CSS.getStyleSheetText failed" };
limitationNotes.add("CSS text could not be retrieved for one or more authorized stylesheets.");
items.push(item);
continue;
}
if (sourceText === undefined) {
sourceFailures += 1;
item.source = { included: false, reason: "CSS.getStyleSheetText returned invalid text" };
limitationNotes.add("CSS text was invalid or absent for one or more authorized stylesheets.");
items.push(item);
continue;
}
const bytes = Buffer.byteLength(sourceText, "utf8");
if (bytes > MAX_SHEET_BYTES) {
sourceFailures += 1;
truncated = true;
item.source = { included: false, reason: "stylesheet source exceeds the per-stylesheet byte limit" };
limitationNotes.add(`CSS source capture omitted text larger than ${String(MAX_SHEET_BYTES)} bytes per stylesheet.`);
items.push(item);
continue;
}
if (includedBytes + bytes > MAX_TOTAL_BYTES) {
sourceFailures += 1;
truncated = true;
item.source = { included: false, reason: "aggregate stylesheet source byte limit reached" };
limitationNotes.add(`CSS source capture omitted text after reaching the ${String(MAX_TOTAL_BYTES)} byte aggregate limit.`);
items.push(item);
continue;
}
includedBytes += bytes;
item.source = { included: true, artifact: createWebTextArtifact(sourceText, "text/css") };
items.push(item);
}
if (input.request?.include_style_sources !== true)
limitationNotes.add("Stylesheet source capture was not selected; no CSS.getStyleSheetText requests were sent.");
if (excluded > 0)
limitationNotes.add(`${String(excluded)} stylesheet(s) were excluded because their origin or frame was outside the authorized local scope.`);
if (sourceFailures > 0)
limitationNotes.add(`${String(sourceFailures)} stylesheet source(s) failed retrieval or exceeded a source budget.`);
input.limitations.push(...limitationNotes);
return {
total: styles.length,
items: items.sort((left, right) => compareUnicodeCodePoints(left.style_sheet_key, right.style_sheet_key)),
excluded,
source_failures: sourceFailures,
truncated,
limits: {
max_sheets: MAX_SHEETS,
max_sheet_bytes: MAX_SHEET_BYTES,
max_total_bytes: MAX_TOTAL_BYTES,
},
};
};
const authorizedStyle = async (style, frameIds) => {
if (style.frameId === null || !frameIds.has(style.frameId) || style.origin !== "regular")
return undefined;
const sourceURL = style.sourceURL ?? "";
if (sourceURL === "") {
if (!style.isInline && !style.isConstructed)
return undefined;
return { frameId: style.frameId, filePath: null };
}
const filePath = await authorizedElectronFile(sourceURL);
return filePath === undefined ? undefined : { frameId: style.frameId, filePath };
};
const recordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
? value
: undefined;
const stringValue = (value) => typeof value === "string" ? value : undefined;
@@ -0,0 +1,122 @@
import { createWebScreenshotArtifact, decodeCanonicalBase64 } from '../domain/webScreenshot.js';
import { readPngHeaderDimensions } from '../domain/pngHeader.js';
import { authorizedElectronFile } from './ElectronFileScope.js';
export async function runElectronVisualProbe(context,frames,send,makeCleanupSend) {
const probe=context.input.visual_probe;
if(!probe||probe.actions.length===0)return undefined;
const result={status:'completed',steps:[],frames:[],restoration:[],limitations:[
'Native debugging input only; no renderer JavaScript evaluation or model request is performed by this provider.',
'Frames are sequential PNG samples with measured timestamps, not a guaranteed video frame rate.',
'Viewport and media cleanup clear the overrides; any overrides that existed before this request were not observable.',
'Click restoration is suitable for caller-selected toggles only; other application effects are not rolled back.',
'Scroll restoration applies the inverse wheel input; clipping, scroll inertia and layout changes can prevent exact position restoration.',
'Keyboard focus and other keyboard effects are not restored. Pointer cleanup moves to the viewport origin, not the prior pointer position.'
,'Text replacement is limited to visible, enabled single-line text/search inputs; it does not submit the input or restore its prior value.'
]};
const start=performance.now(),undo=[];let viewport=false,media=false,hover=false,totalBytes=0;
const main=frames[0];
const stable=async()=>{
const tree=await send('Page.getFrameTree',{});const frame=tree.frameTree?.frame;
if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw new Error('Selected Electron frame changed');
};
const select=async(selector,index)=>{
const document=await send('DOM.getDocument',{depth:0});
const selected=await send('DOM.querySelectorAll',{nodeId:document.root.nodeId,selector});
if(index===undefined&&selected.nodeIds.length!==1)throw new Error('Selector must match exactly one node or supply an explicit index');
const nodeId=selected.nodeIds[index??0];if(!nodeId)throw new Error('Selected node is absent');
return nodeId;
};
const point=async(nodeId)=>{
const box=(await send('DOM.getBoxModel',{nodeId})).model?.content;
if(!box||box.length!==8)throw new Error('Selected node has no visible box');
const x=(box[0]+box[2]+box[4]+box[6])/4,y=(box[1]+box[3]+box[5]+box[7])/4;
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
if(x<0||y<0||x>=viewport.clientWidth||y>=viewport.clientHeight)throw new Error('Selected node is outside the visible viewport');
const hit=await send('DOM.getNodeForLocation',{x:Math.floor(x),y:Math.floor(y),includeUserAgentShadowDOM:false});
const target=(await send('DOM.describeNode',{nodeId,depth:-1,pierce:false})).node;
let visited=0;const contains=node=>{
if(!node||++visited>5000)return false;
return node.backendNodeId===hit.backendNodeId||(node.children??[]).some(contains);
};
if(!hit.backendNodeId||!contains(target))throw new Error('Selected node center is clipped or covered by another element');
return {x,y};
};
const click=async(action)=>{
const at=await point(await select(action.selector,action.index));
await send('Input.dispatchMouseEvent',{type:'mouseMoved',...at});
await send('Input.dispatchMouseEvent',{type:'mousePressed',...at,button:'left',clickCount:1});
await send('Input.dispatchMouseEvent',{type:'mouseReleased',...at,button:'left',clickCount:1});
hover=true;
};
const scroll=async(action)=>{
const at=await point(await select(action.selector,action.index));
await send('Input.dispatchMouseEvent',{type:'mouseWheel',...at,deltaX:0,deltaY:action.delta_y});
};
const sample=async(step_id)=>{
await stable();
const captured=await send('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false});
const bytes=decodeCanonicalBase64(captured.data);
if(!bytes||!readPngHeaderDimensions(bytes))throw new Error('Invalid scenario PNG');
totalBytes+=bytes.length;if(totalBytes>32*1024*1024)throw new Error('Scenario PNG budget exceeded');
const regions=[];
for(const selector of probe.track_selectors){
let bounds=null;
try{const b=(await send('DOM.getBoxModel',{nodeId:await select(selector)})).model.border;const xs=[b[0],b[2],b[4],b[6]],ys=[b[1],b[3],b[5],b[7]];bounds=[Math.min(...xs),Math.min(...ys),Math.max(...xs)-Math.min(...xs),Math.max(...ys)-Math.min(...ys)];}catch{}
regions.push({selector,bounds});
}
result.frames.push({step_id,at_ms:performance.now()-start,artifact:createWebScreenshotArtifact(bytes),regions});
};
const pause=milliseconds=>new Promise((resolve,reject)=>{const timer=setTimeout(done,milliseconds);function done(){context.signal?.removeEventListener('abort',abort);resolve()}function abort(){clearTimeout(timer);context.signal?.removeEventListener('abort',abort);reject(new Error('Visual probe cancelled'))}context.signal?.addEventListener('abort',abort,{once:true});if(context.signal?.aborted)abort()});
try{
for(const action of probe.actions){
await stable();
try{
if(action.kind==='click'){await click(action);if(action.restore)undo.push({kind:'click',action});}
else if(action.kind==='hover'){await send('Input.dispatchMouseEvent',{type:'mouseMoved',...await point(await select(action.selector,action.index))});hover=true;}
else if(action.kind==='scroll'){await scroll(action);if(action.restore)undo.push({kind:'scroll',action:{...action,delta_y:-action.delta_y}});}
else if(action.kind==='viewport'){await send('Emulation.setDeviceMetricsOverride',{width:action.width,height:action.height,deviceScaleFactor:0,mobile:false});viewport=true;}
else if(action.kind==='media'){await send('Emulation.setEmulatedMedia',{features:[{name:'prefers-reduced-motion',value:action.reduced_motion}]});media=true;}
else if(action.kind==='wait')await pause(action.duration_ms);
else if(action.kind==='text'){
const nodeId=await select(action.selector,action.index);
await point(nodeId);
const node=(await send('DOM.describeNode',{nodeId})).node,raw=node.attributes??[];
const attrs=Object.fromEntries(Array.from({length:raw.length/2},(_,index)=>[raw[index*2].toLowerCase(),raw[index*2+1]]));
if(node.nodeName!=='INPUT'||!['text','search'].includes((attrs.type??'text').toLowerCase())||'readonly'in attrs||'disabled'in attrs)throw new Error('Text replacement requires an enabled text/search INPUT');
await send('DOM.focus',{nodeId});
const selectAll={key:'a',code:'KeyA',windowsVirtualKeyCode:65,nativeVirtualKeyCode:65,modifiers:2};
await send('Input.dispatchKeyEvent',{type:'keyDown',...selectAll});
await send('Input.dispatchKeyEvent',{type:'keyUp',...selectAll});
const remove={key:'Backspace',code:'Backspace',windowsVirtualKeyCode:8,nativeVirtualKeyCode:8,modifiers:0};
await send('Input.dispatchKeyEvent',{type:'keyDown',...remove});
await send('Input.dispatchKeyEvent',{type:'keyUp',...remove});
if(action.text)await send('Input.insertText',{text:action.text});
}
else if(action.kind==='key'){
if(action.selector){const nodeId=await select(action.selector,action.index);const node=(await send('DOM.describeNode',{nodeId})).node;const attrs=node.attributes??[];
if(['Enter',' '].includes(action.key)&&(/^(INPUT|TEXTAREA)$/i.test(node.nodeName)||attrs.includes('contenteditable')))throw new Error('Activation key on editable content is excluded');
await send('DOM.focus',{nodeId});
}else if(['Enter',' '].includes(action.key))throw new Error('Activation keys require an explicit noneditable target');
const codes={Escape:27,Enter:13,Tab:9,ArrowDown:40,ArrowUp:38,ArrowLeft:37,ArrowRight:39,Home:36,End:35,' ':32,'+':187,'-':189,'0':48};
const packet={key:action.key,windowsVirtualKeyCode:codes[action.key],nativeVirtualKeyCode:codes[action.key],modifiers:action.modifiers};
await send('Input.dispatchKeyEvent',{type:'keyDown',...packet,...(action.key==='Enter'?{text:'\r'}:action.key===' '?{text:' '}:{})});
await send('Input.dispatchKeyEvent',{type:'keyUp',...packet});
}
const actionCompleted=performance.now()-start;
for(let n=0;n<action.frames;n++){if(n)await pause(action.interval_ms);await sample(action.step_id);}
result.steps.push({step_id:action.step_id,kind:action.kind,status:'completed',error:null,action_completed_at_ms:actionCompleted});
}catch(error){result.status='failed';result.steps.push({step_id:action.step_id,kind:action.kind,status:'failed',error:String(error.message??error)});break;}
}
}finally{
if(makeCleanupSend)send=makeCleanupSend();
if(viewport){try{await send('Emulation.clearDeviceMetricsOverride',{});result.restoration.push({kind:'viewport',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'viewport',status:'failed',error:String(error.message??error)});}}
for(const restoration of undo.reverse()){
try{await stable();if(restoration.kind==='click')await click(restoration.action);else await scroll(restoration.action);result.restoration.push({kind:restoration.kind,status:'completed',error:null});}
catch(error){result.status='failed';result.restoration.push({kind:restoration.kind,status:'failed',error:String(error.message??error)});}
}
if(media){try{await send('Emulation.setEmulatedMedia',{features:[]});result.restoration.push({kind:'media',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'media',status:'failed',error:String(error.message??error)});}}
if(hover){try{await send('Input.dispatchMouseEvent',{type:'mouseMoved',x:0,y:0});result.restoration.push({kind:'neutral-pointer',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'neutral-pointer',status:'failed',error:String(error.message??error)});}}
}
return result;
}
+119
View File
@@ -0,0 +1,119 @@
import { createWebScreenshotArtifact, decodeCanonicalBase64 } from '../domain/webScreenshot.js';
import { readPngHeaderDimensions } from '../domain/pngHeader.js';
import { BrowserObservationError } from '../domain/browserObservationError.js';
import { authorizedElectronFile } from './ElectronFileScope.js';
import { runElectronVisualProbe } from './CdpElectronVisualProbe.js';
import { captureElectronPlatformFonts } from './CdpElectronPlatformFonts.js';
import { captureElectronMediaAssets } from './CdpElectronMediaAssets.js';
export const ELECTRON_LAYOUT_PROPERTIES = ['display', 'position', 'font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing', 'color', 'background-color', 'border-radius', 'border-top-width', 'border-top-color', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'gap', 'align-items', 'justify-content', 'box-shadow', 'overflow', 'max-width', 'min-height','transition-property','transition-duration','transition-timing-function','opacity','visibility','transform','text-rendering','-webkit-font-smoothing','font-kerning','font-synthesis','font-optical-sizing','font-feature-settings','text-shadow','zoom','-webkit-text-size-adjust'];
// Optional bounded message DOM text for reproducing identical fixture content.
// Attribute values outside the style identity allowlist and all editable/input subtrees are omitted.
export function captureElectronMessageContent(snapshot,frames) {
const strings=snapshot.strings??[],value=i=>strings[i]??'';
const document=snapshot.documents?.find(d=>value(d.frameId)===frames[0].frame_id);
if(!document)return {nodes:[],text_characters:0,truncated:false};
const raw=document.nodes,selected=[],roots=new Set(),excluded=new Set();let characters=0,truncated=false;
const siblingCounts=new Map(),ordinals=new Map();
for(let n=0;n<(raw.nodeName?.length??0);n++){const tag=value(raw.nodeName[n]);if(tag.startsWith('#'))continue;const key=raw.parentIndex[n]+'|'+tag;const count=(siblingCounts.get(key)??0)+1;siblingCounts.set(key,count);ordinals.set(n,count)}
const classOf=n=>{const attrs=raw.attributes?.[n]??[];for(let a=0;a<attrs.length;a+=2)if(value(attrs[a])==='class')return value(attrs[a+1]).split(/\s+/);return []};
const messageLists=raw.nodeName.map((_,n)=>n).filter(n=>classOf(n).includes('cr-message-list'));
const selectorFor=index=>{const parts=[];for(let n=index;n>=0;n=raw.parentIndex[n]){if(messageLists.length===1&&n===messageLists[0]){parts.unshift('.cr-message-list');break}const tag=value(raw.nodeName[n]);if(!tag.startsWith('#'))parts.unshift(tag.toLowerCase()+':nth-of-type('+ordinals.get(n)+')')}return parts.join(' > ')};
for(let n=0;n<(raw.nodeName?.length??0);n++){
const attrs=raw.attributes?.[n]??[],attributes={};
for(let a=0;a<attrs.length;a+=2)if(['class','id','role','lang','data-language'].includes(value(attrs[a])))attributes[value(attrs[a])]=value(attrs[a+1]);
const names=(attributes.class??'').split(/\s+/),tag=value(raw.nodeName[n]),parent=raw.parentIndex[n];
if(names.includes('cr-self-message')||names.includes('cr-agent'))roots.add(n);
if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag)||excluded.has(parent)||attrs.some((a,i)=>i%2===0&&value(a)==='contenteditable'&&value(attrs[i+1])!=='false'))excluded.add(n);
let inside=false;for(let p=n;p>=0;p=raw.parentIndex[p])if(roots.has(p)){inside=true;break}
if(!inside||excluded.has(n))continue;
if(selected.length>=5000){truncated=true;break}
const text=tag==='#text'?value(raw.nodeValue[n]):'';
if(characters+text.length>65536){truncated=true;break}
characters+=text.length;
selected.push({node_index:n,parent_index:parent,node_name:tag,attributes,...(tag==='#text'?{text}:{}),...(names.some(name=>['cr-agent','cr-self-message','cr-collapse'].includes(name))?{selector:selectorFor(n)}:{})});
}
return {nodes:selected,text_characters:characters,truncated};
}
// Called only after inspectCdpElectronPage has authorized the selected file page.
export async function captureElectronVisuals(context, frames) {
if (!context.input.include_visuals) return undefined;
const signal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000);
const send = async (method, params) => {
if (process.env.REA_VISUAL_TRACE === '1') process.stderr.write(`[REA visuals] ${method}\n`);
return context.connection.send(method, params, context.sessionId, signal);
};
const metrics = await send('Page.getLayoutMetrics', {});
let environmentGpu;
if (context.input.visual_probe?.include_fonts) {
try {
// Browser metadata only; no renderer evaluation or GPU benchmark.
const {gpu} = await context.connection.send('SystemInfo.getInfo', {}, undefined, signal);
const allowed = ['inProcessGpu','skiaBackendType','glImplementationParts','subpixelFontRendering','sandboxed','passthroughCmdDecoder','targetCpuBits','driverVendor','driverVersion','displayType'];
const aux = Object.fromEntries(allowed.filter(key => ['string','number','boolean'].includes(typeof gpu.auxAttributes?.[key]) || gpu.auxAttributes?.[key] === null).map(key => [key,gpu.auxAttributes[key]]));
environmentGpu = { status:'observed', devices:(gpu.devices??[]).slice(0,8).map(device=>({vendor_id:device.vendorId,device_id:device.deviceId,...(typeof device.deviceString==='string'?{device_string:device.deviceString.slice(0,256)}:{}),...(typeof device.active==='boolean'?{active:device.active}:{})})),aux_attributes:aux,feature_status:Object.fromEntries(Object.entries(gpu.featureStatus??{}).filter(([key,value])=>key.length<100&&typeof value==='string'&&value.length<100)) };
} catch { environmentGpu = { status:'unavailable' }; }
}
if (context.input.visual_probe) { await send('DOM.enable', {}); await send('CSS.enable', {}); }
let environmentMedia;
if(context.input.visual_probe){
try{const media=await send('CSS.getMediaQueries',{});const reduced=(media.medias??[]).find(m=>/^\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)$/i.test(m.text??''));environmentMedia={reduced_motion:reduced?.mediaList?.[0]?.active??null};}
catch{environmentMedia={reduced_motion:null}}
}
const snapshot = await send('DOMSnapshot.captureSnapshot', { computedStyles: ELECTRON_LAYOUT_PROPERTIES, includePaintOrder: false, includeDOMRects: true });
const strings = snapshot.strings ?? [];
const str = index => typeof strings[index] === 'string' ? strings[index] : '';
const frameIds = new Set(frames.map(frame => frame.frame_id));
const documents = [];
for (const document of snapshot.documents ?? []) {
if (!frameIds.has(str(document.frameId)) || await authorizedElectronFile(str(document.documentURL)) === undefined) continue;
const nodes = document.nodes ?? {};
const layout = document.layout ?? {};
const items = [];
const svgNodes = [];
const svgTags = new Set(['svg', 'path', 'g', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse', 'defs', 'clippath', 'mask']);
const svgAttributes = new Set(['class', 'id', 'viewBox', 'width', 'height', 'd', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'fill', 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'transform', 'clip-path', 'opacity']);
for (let n = 0; n < (nodes.nodeName?.length ?? 0); n++) {
const tag = str(nodes.nodeName[n]);
if (!svgTags.has(tag.toLowerCase())) continue;
const attributes = {};
const attrs = nodes.attributes?.[n] ?? [];
for (let a = 0; a < attrs.length; a += 2) if (svgAttributes.has(str(attrs[a]))) attributes[str(attrs[a])] = str(attrs[a + 1]);
svgNodes.push({ node_index: n, parent_index: nodes.parentIndex?.[n] ?? -1, node_name: tag, attributes });
}
for (let i = 0; i < (layout.nodeIndex?.length ?? 0); i++) {
const node = layout.nodeIndex[i];
const attrs = nodes.attributes?.[node] ?? [];
const attributes = {};
for (let a = 0; a < attrs.length; a += 2) {
const key = str(attrs[a]);
// Retain style identity only; never input values, URL attributes or hidden payloads.
if (['class', 'id', 'role'].includes(key)) attributes[key] = str(attrs[a + 1]);
}
const styles = {};
for (let p = 0; p < ELECTRON_LAYOUT_PROPERTIES.length; p++) styles[ELECTRON_LAYOUT_PROPERTIES[p]] = str(layout.styles?.[i]?.[p]);
const bounds = layout.bounds?.[i];
if (!Array.isArray(bounds) || bounds.length !== 4 || !bounds.every(Number.isFinite)) continue;
items.push({ node_index: node, parent_index: nodes.parentIndex?.[node] ?? -1, node_name: str(nodes.nodeName?.[node]), attributes, bounds, styles });
}
documents.push({ frame_id: str(document.frameId), file_path: await authorizedElectronFile(str(document.documentURL)), scroll_x: document.scrollOffsetX ?? 0, scroll_y: document.scrollOffsetY ?? 0, nodes: items, svg_nodes: svgNodes });
}
const captured = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false });
if (typeof captured.data !== 'string' || captured.data.length > 24 * 1024 * 1024) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
const bytes = decodeCanonicalBase64(captured.data);
const viewport = bytes && readPngHeaderDimensions(bytes);
if (!viewport) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
const css = metrics.cssLayoutViewport ?? metrics.layoutViewport;
if (!css || !Number.isFinite(css.clientWidth) || !Number.isFinite(css.clientHeight)) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
const fonts = context.input.visual_probe?.include_fonts ? await captureElectronPlatformFonts({...snapshot,reaLayoutProperties:ELECTRON_LAYOUT_PROPERTIES,reaViewport:{width:css.clientWidth,height:css.clientHeight,page_x:css.pageX??0,page_y:css.pageY??0}},frames,send) : undefined;
const mediaAssets = await captureElectronMediaAssets(snapshot,frames,context.input.visual_probe?.media_selectors,send);
// Baseline DOM/fonts/media acquisition must not consume the scenario's input budget.
// Both stages remain bounded and still inherit the caller's cancellation.
const probeSignal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000);
const probeSend = (method,params)=>context.connection.send(method,params,context.sessionId,probeSignal);
const probe = await runElectronVisualProbe({...context,signal:probeSignal},frames,probeSend,()=>{const cleanupSignal=AbortSignal.timeout(5000);return (method,params)=>context.connection.send(method,params,context.sessionId,cleanupSignal)});
const environment = { ...(environmentMedia?{environment_media:environmentMedia}:{}), ...(environmentGpu?{environment_gpu:environmentGpu}:{}), ...(mediaAssets?{media_assets:mediaAssets}:{}) };
return { captured_at: new Date().toISOString(), viewport, css_viewport: { width: css.clientWidth, height: css.clientHeight, page_x: css.pageX ?? 0, page_y: css.pageY ?? 0 }, ...environment,artifact: createWebScreenshotArtifact(bytes), layout: { properties: ELECTRON_LAYOUT_PROPERTIES, documents }, ...(fonts ? {font_samples:fonts.samples,font_missing:fonts.missing} : {}), ...(context.input.visual_probe?.include_message_content?{message_content:captureElectronMessageContent(snapshot,frames)}:{}), ...(probe ? {probe} : {}), limitations: ['Baseline captures the visible viewport; optional native visual_probe actions are recorded separately. No renderer JavaScript is evaluated.', 'DOM layout and screenshot are sequential observations, not an atomic rendering snapshot.', 'Layout omits text and input values; include_fonts retains bounded visible samples; optional include_message_content retains bounded message DOM text including collapsed message descendants, excluding editable/input subtrees.'] };
}
@@ -0,0 +1,49 @@
// Native metadata diagnostic owned by the local REA provider. No target injection or termination.
import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs/promises';
import { createEvidence } from '../domain/evidence.js';
export async function inspectDesktopElectronProcesses(processName) {
if (process.platform !== 'win32') throw new Error('Windows process metadata provider requires Windows');
if (!/^[A-Za-z0-9_.-]{1,80}\.exe$/i.test(processName)) throw new Error('Select one literal executable basename');
const script = `$targets=Get-CimInstance Win32_Process -Filter "Name='${processName}'"; $rows=foreach($p in $targets){$debugPort=$null;$processType='main';if($p.CommandLine -match '--remote-debugging-port[= ]+(\\d+)'){$debugPort=[int]$Matches[1]};if($p.CommandLine -match '--type[= ]+([^\\s]+)'){$processType=$Matches[1]};[pscustomobject]@{pid=$p.ProcessId;parentPid=$p.ParentProcessId;executable=$p.ExecutablePath;type=$processType;debugPort=$debugPort}};$listeners=Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue|Where-Object{$_.OwningProcess -in $targets.ProcessId}|Select-Object LocalAddress,LocalPort,OwningProcess;[pscustomobject]@{processes=@($rows);listeners=@($listeners)}|ConvertTo-Json -Depth 5 -Compress`;
const result = await new Promise((resolve,reject)=>{
const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']});
const chunks=[];let size=0;const timer=setTimeout(()=>{child.kill();reject(new Error('Windows metadata query timed out'))},15000);
child.stdout.on('data',chunk=>{size+=chunk.length;if(size>1024*1024){child.kill();reject(new Error('Windows metadata budget exceeded'))}else chunks.push(chunk)});
child.stderr.on('data',()=>{});
child.once('error',error=>{clearTimeout(timer);reject(error)});
child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Windows metadata query failed'));try{resolve(JSON.parse(Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/,'')))}catch(error){reject(error)}});
});
return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron metadata provider',version:'1+state-local.1'},{
predicateType:'rea.desktop-electron-processes',operation:'inspect_desktop_electron_processes',parameters:{process_name:processName},result,
confidence:'observed',authority:'external-service',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'},
limitations:['Native process metadata and listening sockets only; no target injection, file extraction, restart or termination.','Command lines are reduced to process type and declared remote-debugging port; other argument values are omitted.']
});
}
// Explicit lifecycle operation. The caller must already have authorization to interrupt WorkBuddy.
export async function restartDesktopElectronDebug(executable,port,authorized) {
if(process.platform!=='win32'||authorized!==true)throw new Error('An authorized Windows restart is required');
if(!Number.isInteger(port)||port<1024||port>65535)throw new Error('Select one debugging port from 1024 to 65535');
const expected=path.join(process.env.LOCALAPPDATA,'Programs','WorkBuddy','WorkBuddy.exe');
const actual=await fs.realpath(executable),allowed=await fs.realpath(expected);
if(actual.toLowerCase()!==allowed.toLowerCase())throw new Error('Restart is limited to the installed WorkBuddy executable');
const quoted=actual.replaceAll("'","''");
const script=`$ErrorActionPreference='Stop';$exe='${quoted}';$before=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});$close=@();foreach($p in $before){$native=Get-Process -Id $p.ProcessId -ErrorAction SilentlyContinue;if($native -and $native.MainWindowHandle -ne 0){$close+= [pscustomobject]@{pid=$p.ProcessId;requested=$native.CloseMainWindow()}}};$deadline=[datetime]::UtcNow.AddSeconds(8);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 200}while([datetime]::UtcNow -lt $deadline);$forced=@();foreach($p in $before){$current=Get-CimInstance Win32_Process -Filter ("ProcessId="+$p.ProcessId);if($current -and $current.ExecutablePath -eq $exe -and $current.CreationDate -eq $p.CreationDate){Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue;$forced+=$p.ProcessId}};$deadline=[datetime]::UtcNow.AddSeconds(4);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 100}while([datetime]::UtcNow -lt $deadline);if($remaining.Count -ne 0){throw 'Selected WorkBuddy processes did not exit; no replacement was launched'};[pscustomobject]@{beforePids=@($before|ForEach-Object{$_.ProcessId});closeRequests=@($close);forcedPids=@($forced);remaining=0}|ConvertTo-Json -Depth 4 -Compress`;
const stopped=await new Promise((resolve,reject)=>{
const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']});
let output='',errors='';const timer=setTimeout(()=>{child.kill();reject(new Error('Authorized restart shutdown timed out'))},20000);
child.stdout.on('data',b=>{output+=b;if(output.length>65536){child.kill();reject(new Error('Restart result budget exceeded'))}});child.stderr.on('data',b=>errors+=b);
child.once('error',e=>{clearTimeout(timer);reject(e)});child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Restart shutdown failed: '+errors.slice(0,1000)));try{resolve(JSON.parse(output.replace(/^\uFEFF/,'')))}catch(e){reject(e)}});
});
const env={...process.env};for(const key of Object.keys(env))if(key.toUpperCase()==='ELECTRON_RUN_AS_NODE')delete env[key];
const args=[`--remote-debugging-port=${port}`,'--remote-debugging-address=127.0.0.1'];
const launched=spawn(actual,args,{detached:true,windowsHide:true,stdio:'ignore',env});
await new Promise((resolve,reject)=>{launched.once('spawn',resolve);launched.once('error',reject)});launched.unref();
return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron lifecycle provider',version:'1+state-local.2'},{
predicateType:'rea.desktop-electron-restart',operation:'restart_desktop_electron_debug',parameters:{executable:actual,port,restart_authorized:true},result:{...stopped,launchedPid:launched.pid,arguments:args},confidence:'observed',authority:'controlled-replay',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'},
limitations:['Caller-authorized restart can interrupt in-flight tasks.','Graceful close is attempted first; surviving launch identities belonging to the exact selected executable are terminated.','Launch evidence confirms process creation only; inspect the debugging endpoint separately.','No original file contents or application data are extracted by this lifecycle operation.']
});
}
@@ -0,0 +1,49 @@
import { z } from 'zod';
import { webScreenshotArtifactSchema } from '../webScreenshot.js';
const sampling = { step_id:z.string().min(1).max(80), frames:z.number().int().min(0).max(24).default(1), interval_ms:z.number().int().min(16).max(500).default(50) };
const target = {selector:z.string().min(1).max(500),index:z.number().int().min(0).max(100).optional()};
export const electronVisualProbeInputSchema = z.strictObject({
include_fonts:z.boolean().default(false),
include_message_content:z.boolean().default(false),
media_selectors:z.array(z.string().trim().min(1).max(500)).max(4).default([]),
track_selectors:z.array(z.string().min(1).max(500)).max(12).default([]),
actions:z.array(z.discriminatedUnion('kind',[
z.strictObject({...sampling,...target,kind:z.literal('hover')}),
z.strictObject({...sampling,...target,kind:z.literal('click'),restore:z.boolean().default(false)}),
z.strictObject({...sampling,...target,kind:z.literal('text'),text:z.string().max(2048).refine(value=>!/[\u0000-\u001f\u007f]/u.test(value),'Single-line text only')}),
z.strictObject({...sampling,...target,kind:z.literal('scroll'),delta_y:z.number().min(-3000).max(3000),restore:z.boolean().default(false)}),
z.strictObject({...sampling,kind:z.literal('key'),selector:z.string().min(1).max(500).optional(),index:z.number().int().min(0).max(100).optional(),key:z.enum(['Escape','Enter','Tab','ArrowDown','ArrowUp','ArrowLeft','ArrowRight','Home','End',' ','+','-','0']),modifiers:z.number().int().min(0).max(15).default(0)}),
z.strictObject({...sampling,kind:z.literal('wait'),duration_ms:z.number().int().min(0).max(2000)}),
z.strictObject({...sampling,kind:z.literal('viewport'),width:z.number().int().min(160).max(3840),height:z.number().int().min(160).max(2160)})
,z.strictObject({...sampling,kind:z.literal('media'),reduced_motion:z.enum(['reduce','no-preference'])})
])).max(12).default([])
}).superRefine((value,ctx)=>{
if(value.actions.reduce((n,a)=>n+a.frames,0)>64)ctx.addIssue({code:'custom',message:'At most 64 scenario frames'});
if(value.actions.reduce((n,a)=>n+a.frames*a.interval_ms+(a.duration_ms??0),0)>15000)ctx.addIssue({code:'custom',message:'Declared sampling and waits must fit 15 seconds'});
if(new Set(value.actions.map(a=>a.step_id)).size!==value.actions.length)ctx.addIssue({code:'custom',message:'Step IDs must be unique'});
if(new Set(value.media_selectors).size!==value.media_selectors.length)ctx.addIssue({code:'custom',message:'Media selectors must be unique'});
});
const mediaIdentity = {
selector:z.string(),frame_id:z.string(),node_index:z.number().int().optional(),backend_node_id:z.number().int().optional(),
selected_node:z.object({node_name:z.literal('IMG'),attributes:z.record(z.string(),z.string()),current_source_url:z.string().nullable()}).optional(),
visibility:z.object({status:z.enum(['observed','covered','unknown']),reason:z.string().optional()}).optional(),
visibility_samples:z.array(z.object({x:z.number(),y:z.number(),hit_backend_node_id:z.number().int().optional(),hit_node_index:z.number().int().optional(),hit_node_name:z.string().optional(),hit_class:z.string().optional()})).max(9).optional(),
source:z.object({url:z.string().nullable(),url_sha256:z.string().regex(/^[a-f0-9]{64}$/),selection:z.enum(['current_source_url','src']),mime_type:z.string()}).optional()
};
export const electronMediaAssetsSchema = z.object({
items:z.array(z.discriminatedUnion('status',[
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()})
])).max(4),
total_bytes:z.number().int().min(0).max(8388608),
limits:z.object({max_assets:z.literal(4),max_asset_bytes:z.literal(2097152),max_total_bytes:z.literal(8388608)}),
limitations:z.array(z.string())
});
export const platformFontSampleSchema = z.object({category:z.string(),text:z.string().max(256),fontFamily:z.string(),codeContext:z.enum(['inline','block']).optional(),platformFonts:z.array(z.object({familyName:z.string(),postScriptName:z.string(),isCustomFont:z.boolean(),glyphCount:z.number().int().min(0)}))});
export const visualProbeOutputSchema = z.object({
status:z.enum(['completed','failed']),
steps:z.array(z.object({step_id:z.string(),kind:z.string(),status:z.enum(['completed','failed']),error:z.string().nullable(),action_completed_at_ms:z.number().optional()})),
frames:z.array(z.object({step_id:z.string(),at_ms:z.number(),artifact:webScreenshotArtifactSchema,regions:z.array(z.object({selector:z.string(),bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]).nullable()}))})),
restoration:z.array(z.object({kind:z.string(),status:z.enum(['completed','failed']),error:z.string().nullable()})),
limitations:z.array(z.string())
});
+225
View File
@@ -0,0 +1,225 @@
import { createHash } from 'node:crypto'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
const MAX_SELECTED_FILES = 16
const MAX_FILE_BYTES = 8 * 1024 * 1024
const MAX_TOTAL_BYTES = 16 * 1024 * 1024
const SHA256_PATTERN = /^[a-f0-9]{64}$/u
function assertCanonicalSelectedPath(value, binary = false) {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError('Selected paths must be non-empty relative strings')
}
if (
value.startsWith('/') ||
value.endsWith('/') ||
value.includes('\\') ||
value.includes('\0') ||
/^[a-zA-Z]:/u.test(value) ||
value.split('/').some((part) => part === '' || part === '.' || part === '..') ||
path.posix.normalize(value) !== value
) {
throw new TypeError(`Selected path is not canonical: ${value}`)
}
if (binary ? !/\.(?:png|jpe?g|gif|webp|svg|woff2?|ttf)$/u.test(value) : !/\.(?:js|css|json|md|txt|yaml|yml|py)$/u.test(value)) {
if (binary) throw new TypeError(`Selected binary path must name a supported image or font: ${value}`)
throw new TypeError(`Selected path must name a supported UTF-8 text file (.js/.css/.json/.md/.txt/.yaml/.yml/.py): ${value}`)
}
}
function validateSelection(expectedRootSha256, selectedPaths, binary = false) {
if (typeof expectedRootSha256 !== 'string' || !SHA256_PATTERN.test(expectedRootSha256)) {
throw new TypeError('Expected ASAR root SHA-256 must be 64 lowercase hexadecimal characters')
}
if (!Array.isArray(selectedPaths) || selectedPaths.length === 0 || selectedPaths.length > MAX_SELECTED_FILES) {
throw new RangeError(`Select between 1 and ${MAX_SELECTED_FILES} exact files`)
}
const paths = [...selectedPaths]
const seen = new Set()
for (const selectedPath of paths) {
assertCanonicalSelectedPath(selectedPath, binary)
if (seen.has(selectedPath)) throw new TypeError(`Duplicate selected path: ${selectedPath}`)
seen.add(selectedPath)
}
return paths
}
async function loadReaRuntime() {
const runtimeRoot = path.resolve(process.env.REA_RUNTIME_ROOT || '.tmp/rea-css-runtime')
const runtimeUrl = (relativePath) => pathToFileURL(path.join(runtimeRoot, relativePath)).href
const [readerModule, evidenceModule] = await Promise.all([
import(runtimeUrl('dist/artifacts/AsarArtifactReader.js')),
import(runtimeUrl('dist/domain/evidence.js')),
])
const { createRequire } = await import('node:module')
const requireFromRuntime = createRequire(path.join(runtimeRoot, 'package.json'))
const packageMetadata = requireFromRuntime('./package.json')
return {
AsarArtifactReader: readerModule.AsarArtifactReader,
createEvidence: evidenceModule.createEvidence,
version: packageMetadata.version,
}
}
async function readSelectedEntry(reader, entry, totalBytes, binary = false) {
if (entry.kind !== 'file') throw new TypeError(`Selected path is not a regular file: ${entry.path}`)
if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) {
throw new Error(`Selected file has no valid declared size: ${entry.path}`)
}
if (entry.declaredSize > MAX_FILE_BYTES) {
throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${entry.path}`)
}
if (typeof entry.declaredSha256 !== 'string' || !SHA256_PATTERN.test(entry.declaredSha256)) {
throw new Error(`Selected file has no valid declared SHA-256: ${entry.path}`)
}
const chunks = []
const digest = createHash('sha256')
let bytes = 0
const stream = await reader.open(entry)
for await (const value of stream) {
if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) {
throw new TypeError(`Selected file stream returned non-byte data: ${entry.path}`)
}
const chunk = Buffer.from(value)
bytes += chunk.length
if (bytes > MAX_FILE_BYTES || bytes > entry.declaredSize || totalBytes + bytes > MAX_TOTAL_BYTES) {
throw new RangeError(`Selected text exceeds its declared size or aggregate budget: ${entry.path}`)
}
digest.update(chunk)
chunks.push(chunk)
}
if (bytes !== entry.declaredSize) {
throw new Error(`Selected file size mismatch for ${entry.path}: declared ${entry.declaredSize}, read ${bytes}`)
}
const actualSha256 = digest.digest('hex')
if (actualSha256 !== entry.declaredSha256) {
throw new Error(`Selected file SHA-256 mismatch for ${entry.path}`)
}
if (binary) return { path: entry.path, size_bytes: bytes, sha256: actualSha256, unpacked: entry.unpacked,
encoding: 'base64', data_base64: Buffer.concat(chunks, bytes).toString('base64') }
let text
try {
text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks, bytes))
} catch (cause) {
throw new Error(`Selected file is not strict UTF-8: ${entry.path}`, { cause })
}
return {
path: entry.path,
size_bytes: bytes,
sha256: actualSha256,
unpacked: entry.unpacked,
text,
}
}
/** Export bounded UTF-8 text from exact selected files in a root-pinned ASAR. */
export async function exportSelectedArtifactText(inputPath, expectedRootSha256, selectedPaths, binary = false) {
if (typeof inputPath !== 'string' || inputPath.length === 0) {
throw new TypeError('ASAR input path must be a non-empty string')
}
const paths = validateSelection(expectedRootSha256, selectedPaths, binary)
const { AsarArtifactReader, createEvidence, version } = await loadReaRuntime()
const reader = new AsarArtifactReader(inputPath)
let primaryFailure
let primaryFailed = false
let evidence
try {
await reader.prepareContainer(expectedRootSha256)
const wanted = new Set(paths)
const entries = new Map()
for await (const entry of reader.entries()) {
if (!wanted.has(entry.path)) continue
if (entries.has(entry.path)) throw new Error(`ASAR inventory contains a duplicate path: ${entry.path}`)
entries.set(entry.path, entry)
}
for (const selectedPath of paths) {
if (!entries.has(selectedPath)) throw new Error(`Selected path is absent from the ASAR inventory: ${selectedPath}`)
}
let declaredTotalBytes = 0
for (const selectedPath of paths) {
const entry = entries.get(selectedPath)
if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) {
throw new Error(`Selected file has no valid declared size: ${selectedPath}`)
}
if (entry.declaredSize > MAX_FILE_BYTES) {
throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${selectedPath}`)
}
declaredTotalBytes += entry.declaredSize
if (declaredTotalBytes > MAX_TOTAL_BYTES) {
throw new RangeError(`Selected files exceed the ${MAX_TOTAL_BYTES}-byte aggregate budget`)
}
}
const files = []
let actualTotalBytes = 0
for (const selectedPath of paths) {
const file = await readSelectedEntry(reader, entries.get(selectedPath), actualTotalBytes, binary)
actualTotalBytes += file.size_bytes
files.push(file)
}
const unselectedScope = 'Not read; unpacked payload content and availability are unknown.'
evidence = createEvidence(
{ path: inputPath, sha256: expectedRootSha256, format: 'asar' },
{
id: binary ? 'rea-selected-asar-binary' : 'rea-selected-asar-text',
name: binary ? 'REA selected ASAR binary adapter' : 'REA selected ASAR text adapter',
version,
},
{
predicateType: binary ? 'rea.artifact.selected-binary' : 'rea.artifact.selected-text',
operation: binary ? 'export_selected_asar_artifact_binary' : 'export_selected_asar_artifact_text',
parameters: {
expected_root_sha256: expectedRootSha256,
selected_paths: paths,
max_file_bytes: MAX_FILE_BYTES,
max_total_bytes: MAX_TOTAL_BYTES,
},
result: {
root_sha256: expectedRootSha256,
scope: {
kind: 'selected-files-only',
selected_paths: paths,
unselected_members: unselectedScope,
},
files,
},
confidence: 'observed',
authority: 'shipped-artifact',
limitations: [
'Only selected file streams were read and validated.',
`Unselected ASAR members were not opened; ${unselectedScope}`,
],
locations: paths.map((selectedPath) => ({ kind: 'artifact-path', path: selectedPath })),
},
)
} catch (error) {
primaryFailure = error
primaryFailed = true
}
try {
await reader.close()
} catch (cleanupFailure) {
if (primaryFailed) {
throw new AggregateError(
[primaryFailure, cleanupFailure],
'Selected ASAR text export failed and reader cleanup also failed',
{ cause: primaryFailure },
)
}
throw cleanupFailure
}
if (primaryFailed) throw primaryFailure
return evidence
}
@@ -0,0 +1,110 @@
import { createHash } from 'node:crypto'
import path from 'node:path'
import { DirectoryArtifactReader } from './DirectoryArtifactReader.js'
import { createEvidence } from '../domain/evidence.js'
const MAX_BYTES = 32 * 1024 * 1024
const MAX_ENTRIES = 20000
export async function exportSelectedConversationText(projectsRoot, sessionId, includeImages = false) {
if (!path.isAbsolute(projectsRoot) || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(sessionId)) {
throw new TypeError('An absolute projects root and exact UUID session ID are required')
}
const reader = new DirectoryArtifactReader(projectsRoot)
const matches = []
let examined = 0
try {
for await (const entry of reader.entries()) {
if (++examined > MAX_ENTRIES) throw new RangeError('Conversation inventory entry budget exceeded')
if (entry.path.split('/').at(-1) === `${sessionId}.jsonl`) matches.push(entry)
}
if (matches.length !== 1) throw new Error(`Expected one exact session transcript, found ${matches.length}`)
const entry = matches[0]
if (entry.kind !== 'file' || !Number.isSafeInteger(entry.declaredSize) || entry.declaredSize > MAX_BYTES) {
throw new RangeError('Selected transcript is not a bounded regular file')
}
const chunks = []
let bytes = 0
for await (const chunk of await reader.open(entry)) {
const data = Buffer.from(chunk)
bytes += data.length
if (bytes > MAX_BYTES) throw new RangeError('Transcript grew beyond the byte budget')
chunks.push(data)
}
const data = Buffer.concat(chunks)
if (data.length !== entry.declaredSize) throw new Error('Transcript size changed during read')
const text = new TextDecoder('utf-8', { fatal: true }).decode(data)
const sha256 = createHash('sha256').update(data).digest('hex')
let records = 0
const imageRefs = new Map()
for (const line of text.split(/\r?\n/)) {
if (!line.trim()) continue
const record = JSON.parse(line)
if (!record || typeof record !== 'object' || Array.isArray(record)) throw new Error('JSONL record is not an object')
if (record.sessionId && record.sessionId !== sessionId) throw new Error('Transcript contains another session ID')
if (includeImages && record.type === 'message' && Array.isArray(record.content)) {
for (const block of record.content) {
const image = block?.type === 'input_image' ? block.image : undefined
if (image?.type !== 'image_blob_ref') continue
if (!/^[a-f0-9]{64}$/.test(image.blob_id) || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mime)) {
throw new Error('Unsupported or invalid transcript image reference')
}
imageRefs.set(image.blob_id, image)
}
}
records++
}
const attachments = includeImages ? await readImages(projectsRoot, [...imageRefs.values()]) : []
return createEvidence({ path: path.join(projectsRoot, entry.path), sha256, format: 'file' },
{ id: 'rea-selected-conversation-text', name: 'REA selected local conversation transcript', version: '1+local.1' }, {
predicateType: 'rea.conversation.selected-text', operation: 'export_selected_conversation_text',
parameters: { projects_root: projectsRoot, session_id: sessionId, include_images: includeImages, max_bytes: MAX_BYTES, max_entries: MAX_ENTRIES },
result: { session_id: sessionId, file: { path: entry.path, sha256, bytes, records, text }, attachments,
scope: 'One exact session JSONL and optionally its referenced image blobs; other file contents are not read' },
confidence: 'observed', authority: 'historical-reference',
limitations: ['Stable directory-reader identity checks do not establish an atomic snapshot of an actively written conversation.',
'Exports stored history only; non-image external files and live kernel state are not copied.'],
locations: [{ kind: 'artifact-path', path: entry.path }]
})
} finally { await reader.close() }
}
async function readImages(projectsRoot, references) {
if (!references.length) return []
if (references.length > 32) throw new RangeError('Too many image references')
const root = path.resolve(projectsRoot, '../blobs')
const selected = new Map()
for (const ref of references) {
const target = path.resolve(ref.blob_path)
const relative = path.relative(root, target).split(path.sep).join('/')
if (!new RegExp(`^${ref.blob_id.slice(0, 2)}/${ref.blob_id}\\.(png|jpg|jpeg|webp|gif)$`).test(relative)) {
throw new Error('Image reference is outside the selected content-addressed blob root')
}
selected.set(relative, ref)
}
const reader = new DirectoryArtifactReader(root)
const files = []
let total = 0
try {
for await (const entry of reader.entries(undefined, { descendInto: directory => [...selected.keys()].some(file => file.startsWith(directory + '/')) })) {
const ref = selected.get(entry.path)
if (!ref) continue
if (entry.kind !== 'file' || !Number.isSafeInteger(entry.declaredSize) || entry.declaredSize > 8 * 1024 * 1024) {
throw new Error('Referenced image is not a bounded regular file')
}
const chunks = []
for await (const chunk of await reader.open(entry)) {
total += chunk.length
if (total > 16 * 1024 * 1024) throw new RangeError('Image export byte budget exceeded')
chunks.push(Buffer.from(chunk))
}
const data = Buffer.concat(chunks)
const sha256 = createHash('sha256').update(data).digest('hex')
if (sha256 !== ref.blob_id || data.length !== ref.size || data.length !== entry.declaredSize) {
throw new Error('Image blob digest/size does not match the transcript reference')
}
files.push({ blob_id: ref.blob_id, mime: ref.mime, bytes: data.length, sha256, data_base64: data.toString('base64') })
}
if (files.length !== references.length) throw new Error('A referenced image blob is absent')
return files
} finally { await reader.close() }
}