Add policy-bound encrypted agent session restoration and audit traces
This commit is contained in:
1 parent
e8a654fb38
commit
b991ee9da6
27 files changed
+405
-24
No files matched your search
@@ -3,7 +3,7 @@ const http = require('node:http');
|
||||
const crypto = require('node:crypto');
|
||||
const channel=require('../../packages/core/policy-channel.cjs');
|
||||
|
||||
const APP_ACTIONS=Object.freeze(['vault:status','vault:list','vault:fill','history:list','history:go','window:state','window:control','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','library:organize-plan','library:organize-apply']);
|
||||
const APP_ACTIONS=Object.freeze(['sessions:save','sessions:list','sessions:restore','vault:status','vault:list','vault:fill','history:list','history:go','window:state','window:control','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','library:organize-plan','library:organize-apply']);
|
||||
const SCOPES = Object.freeze({ sessions: 'sessions:list', navigate: 'browser:navigate', back:'browser:navigate',forward:'browser:navigate',reload:'browser:navigate', read: 'browser:read', inspect:'browser:read', scroll:'browser:interact', click: 'browser:interact', type: 'browser:interact', select:'browser:interact', check:'browser:interact', ai: 'ai:request',...Object.fromEntries(APP_ACTIONS.map(action=>[action,action])) });
|
||||
const fail = (status, message) => Object.assign(new Error(message), { status });
|
||||
const bounded = (value, max, name) => {
|
||||
@@ -16,9 +16,20 @@ function validate(body) {
|
||||
if (!Object.hasOwn(SCOPES, action)) throw fail(400, 'Unsupported action');
|
||||
const args = body.args ?? {};
|
||||
if (!args || typeof args !== 'object' || Array.isArray(args)) throw fail(400, 'Invalid arguments');
|
||||
const keys = { 'vault:status':['sessionId'],'vault:list':['sessionId'],'vault:fill':['sessionId','credentialRef'],'history:list':['sessionId'],'history:go':['sessionId','snapshotId','entryId'],'window:state':[],'window:control':['command'],sessions: [], navigate: ['sessionId', 'url'],back:['sessionId'],forward:['sessionId'],reload:['sessionId'], read: ['sessionId'], inspect:['sessionId'], scroll:['sessionId','x','y'], click: ['sessionId', 'selector'], type: ['sessionId', 'selector', 'text'], select:['sessionId','selector','value'], check:['sessionId','selector','checked'], ai: ['sessionId', 'prompt'],'application:state':[],'settings:get':[],'settings:update':['appearance'],'bookmarks:list':[],'bookmarks:mutate':['action','bookmark','id','expectedRevision','policyVersion'],'tabs:create':['group','url'],'tabs:activate':['sessionId'],'tabs:close':['sessionId'],'tabs:group':['sessionId','group'],'library:organize-plan':[],'library:organize-apply':['proposalId','confirm','autoApply'] }[action];
|
||||
const keys = { 'sessions:save':['name','sessionIds','id','expectedRevision'],'sessions:list':[],'sessions:restore':['id','planId'],'vault:status':['sessionId'],'vault:list':['sessionId'],'vault:fill':['sessionId','credentialRef'],'history:list':['sessionId'],'history:go':['sessionId','snapshotId','entryId'],'window:state':[],'window:control':['command'],sessions: [], navigate: ['sessionId', 'url'],back:['sessionId'],forward:['sessionId'],reload:['sessionId'], read: ['sessionId'], inspect:['sessionId'], scroll:['sessionId','x','y'], click: ['sessionId', 'selector'], type: ['sessionId', 'selector', 'text'], select:['sessionId','selector','value'], check:['sessionId','selector','checked'], ai: ['sessionId', 'prompt'],'application:state':[],'settings:get':[],'settings:update':['appearance'],'bookmarks:list':[],'bookmarks:mutate':['action','bookmark','id','expectedRevision','policyVersion'],'tabs:create':['group','url'],'tabs:activate':['sessionId'],'tabs:close':['sessionId'],'tabs:group':['sessionId','group'],'library:organize-plan':[],'library:organize-apply':['proposalId','confirm','autoApply'] }[action];
|
||||
if (Object.keys(args).some(k => !keys.includes(k))) throw fail(400, 'Unsupported argument');
|
||||
const clean = {};
|
||||
if(action==='sessions:save'){
|
||||
clean.name=bounded(args.name,80,'session name');
|
||||
if(!Array.isArray(args.sessionIds)||args.sessionIds.length<1||args.sessionIds.length>32||new Set(args.sessionIds).size!==args.sessionIds.length)throw fail(400,'Invalid session selection');
|
||||
clean.sessionIds=args.sessionIds.map(id=>bounded(id,128,'session id'));
|
||||
for(const key of ['id','expectedRevision'])if(args[key]!==undefined)clean[key]=bounded(args[key],36,key);
|
||||
}
|
||||
if(action==='sessions:restore'){
|
||||
if((args.id!==undefined)===(args.planId!==undefined))throw fail(400,'Invalid session restore');
|
||||
const key=args.planId!==undefined?'planId':'id';clean[key]=bounded(args[key],36,key);
|
||||
}
|
||||
if(action.startsWith('sessions:'))for(const key of ['id','expectedRevision','planId'])if(clean[key]!==undefined&&!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(clean[key]))throw fail(400,'Invalid session handle');
|
||||
if(action.startsWith('vault:')){
|
||||
if(args.sessionId!==undefined||action==='vault:fill')clean.sessionId=bounded(args.sessionId,128,'sessionId');
|
||||
if(action==='vault:fill'){clean.credentialRef=bounded(args.credentialRef,36,'credentialRef');if(!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(clean.credentialRef))throw fail(400,'Invalid credential reference');}
|
||||
@@ -59,6 +70,17 @@ function validate(body) {
|
||||
}
|
||||
// Never return adapter objects wholesale: session handles and sanitized text only.
|
||||
function project(action, result) {
|
||||
if(action.startsWith('sessions:')){
|
||||
const handle=value=>{if(typeof value!=='string'||!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value))throw fail(502,'Invalid session handle');return value;};
|
||||
if(action==='sessions:save')return {id:handle(result?.id),revision:handle(result?.revision),tabCount:sessionCount(result?.tabCount)};
|
||||
if(action==='sessions:restore'){
|
||||
if(result?.planId)return {planId:handle(result.planId),id:handle(result.id),tabCount:sessionCount(result.tabCount)};
|
||||
if(result?.completed!==true||!Array.isArray(result.sessionIds)||result.sessionIds.length<1||result.sessionIds.length>32||new Set(result.sessionIds).size!==result.sessionIds.length||!result.sessionIds.includes(result.activeSessionId))throw fail(502,'Invalid restored sessions');
|
||||
return {completed:true,sessionIds:result.sessionIds.map(handle),activeSessionId:handle(result.activeSessionId)};
|
||||
}
|
||||
if(!Array.isArray(result?.collections)||result.collections.length>16)throw fail(502,'Invalid session collections');
|
||||
return {revision:handle(result.revision),collections:result.collections.map(row=>{if(!Array.isArray(row.tabs)||row.tabs.length<1||row.tabs.length>32||!Number.isInteger(row.activeIndex)||row.activeIndex<0||row.activeIndex>=row.tabs.length)throw fail(502,'Invalid session collection');return {id:handle(row.id),name:bounded(row.name,80,'session name'),activeIndex:row.activeIndex,tabs:row.tabs.map(require('./agent-session-adapters.cjs').safeSessionTab)};})};
|
||||
}
|
||||
if(action==='vault:status'){
|
||||
if(!result||!['exists','locked','enabled','allowAutofill'].every(key=>typeof result[key]==='boolean'))throw fail(502,'Invalid vault response');
|
||||
return Object.fromEntries(['exists','locked','enabled','allowAutofill'].map(key=>[key,result[key]]));
|
||||
@@ -100,6 +122,7 @@ function project(action, result) {
|
||||
}
|
||||
return { completed: result?.completed !== false };
|
||||
}
|
||||
function sessionCount(value){if(!Number.isSafeInteger(value)||value<1||value>32)throw fail(502,'Invalid session count');return value;}
|
||||
|
||||
async function startAgentControl({ authorize, actions, audit, credentialFile, persistCredential, onError, traceOperation = async (_name,fn)=>fn(), timeoutMs = 10000, maxRequestsPerMinute = 60, principalMode = 'legacy', principals = [] } = {}) {
|
||||
if(credentialFile!==undefined)throw new TypeError('Plaintext pairing files are prohibited; use protected persistCredential');
|
||||
|
||||
Reference in new issue
Block a user