mecord-connect 1.0.1 → 2.0.1
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 +1 -1
- package/runtime/app/apps/local-agent/src/control-center.js +61 -0
- package/runtime/app/apps/local-agent/src/main.js +142 -14
- package/runtime/app/apps/local-agent/src/migration-proofs.js +194 -0
- package/runtime/app/apps/local-agent/src/relay-agent.js +216 -8
- package/runtime/app/apps/local-agent/src/runtime-factory.js +16 -4
- package/runtime/app/apps/local-agent/src/server.js +1353 -21
- package/runtime/app/src/capabilities/filesystem.js +284 -2
- package/runtime/app/src/capabilities/index.js +2 -0
- package/runtime/app/src/capabilities/perception.js +138 -0
- package/runtime/app/src/capabilities/process.js +371 -2
- package/runtime/app/src/capabilities/sandboxed-compute.js +226 -0
- package/runtime/app/src/capabilities/windows-uia.js +295 -10
- package/runtime/app/src/core/audit.js +88 -0
- package/runtime/app/src/core/authority-kernel.js +209 -0
- package/runtime/app/src/core/capability-policy.js +12 -1
- package/runtime/app/src/core/capability-sdk.js +188 -0
- package/runtime/app/src/core/conversation-ledger.js +146 -0
- package/runtime/app/src/core/desired-state-reconciler.js +90 -0
- package/runtime/app/src/core/desired-state.js +470 -0
- package/runtime/app/src/core/device-pool.js +318 -0
- package/runtime/app/src/core/device-routing.js +31 -6
- package/runtime/app/src/core/digital-operations.js +707 -0
- package/runtime/app/src/core/durable-state.js +20 -5
- package/runtime/app/src/core/enterprise-policy.js +290 -0
- package/runtime/app/src/core/evaluation.js +336 -0
- package/runtime/app/src/core/event-runtime.js +267 -0
- package/runtime/app/src/core/event-ticker.js +66 -0
- package/runtime/app/src/core/execution-optimizer.js +255 -0
- package/runtime/app/src/core/index.js +8 -0
- package/runtime/app/src/core/intent-kernel.js +152 -0
- package/runtime/app/src/core/machine-state.js +2 -1
- package/runtime/app/src/core/organization-coordinator.js +410 -0
- package/runtime/app/src/core/outcome-planner.js +179 -0
- package/runtime/app/src/core/perception-graph.js +365 -0
- package/runtime/app/src/core/perception-publication.js +139 -0
- package/runtime/app/src/core/policy.js +10 -1
- package/runtime/app/src/core/procedure-memory.js +399 -0
- package/runtime/app/src/core/product-identity.js +4 -1
- package/runtime/app/src/core/relay-client.js +64 -3
- package/runtime/app/src/core/resilience-matrix.js +176 -0
- package/runtime/app/src/core/resilience-simulator.js +156 -0
- package/runtime/app/src/core/resource-identity.js +55 -0
- package/runtime/app/src/core/resource-leases.js +249 -0
- package/runtime/app/src/core/runtime.js +13 -11
- package/runtime/app/src/core/semantic-checkpoint.js +420 -0
- package/runtime/app/src/core/side-effect.js +30 -0
- package/runtime/app/src/core/studio-executor.js +527 -0
- package/runtime/app/src/core/studio-teach.js +494 -0
- package/runtime/app/src/core/task-failure.js +61 -0
- package/runtime/app/src/core/task-orchestrator.js +528 -21
- package/runtime/app/src/core/task-store.js +10 -1
- package/runtime/app/src/core/task-verifier.js +154 -0
- package/runtime/app/src/core/task.js +38 -5
- package/runtime/app/src/core/team-coordinator.js +988 -0
- package/runtime/app/src/core/types.js +18 -0
- package/runtime/app/src/core/verification-kernel.js +82 -0
- package/runtime/app/src/core/world-model.js +528 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +224 -49
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function renderControlCenter(nonce ) {
|
|
2
|
+
const safeNonce = nonce.replace(/[^A-Za-z0-9_-]/g, '');
|
|
3
|
+
return '<!doctype html>' +
|
|
4
|
+
'<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' +
|
|
5
|
+
'<title>Mecord Control Center</title>' +
|
|
6
|
+
'<style nonce="' + safeNonce + '">' +
|
|
7
|
+
':root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0b0d10;color:#eef1f5}' +
|
|
8
|
+
'*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 15% 0%,#18212d 0,#0b0d10 42%)}' +
|
|
9
|
+
'button,input,textarea{font:inherit}button{cursor:pointer}.shell{max-width:1440px;margin:auto;padding:24px}.top{display:flex;gap:16px;align-items:center;justify-content:space-between;margin-bottom:20px}' +
|
|
10
|
+
'.brand{display:flex;align-items:center;gap:12px}.mark{width:38px;height:38px;border-radius:12px;background:linear-gradient(145deg,#8aa7ff,#4e68ff);display:grid;place-items:center;font-weight:800;color:#07101f;box-shadow:0 10px 35px #4e68ff33}' +
|
|
11
|
+
'h1{font-size:20px;margin:0}.muted{color:#8e98a8;font-size:13px}.auth{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.input{background:#10151c;border:1px solid #26303d;color:#eef1f5;border-radius:10px;padding:9px 11px;outline:none;min-width:220px}.input:focus{border-color:#6e86ff;box-shadow:0 0 0 3px #6e86ff20}' +
|
|
12
|
+
'.btn{border:1px solid #2c3745;background:#161d26;color:#eef1f5;border-radius:10px;padding:9px 12px}.btn:hover{background:#1c2632}.primary{background:#6178ff;border-color:#7186ff;color:white}.danger{border-color:#713b42;color:#ffb9bf}.warn{border-color:#725d31;color:#ffd88a}' +
|
|
13
|
+
'.status{display:flex;align-items:center;gap:8px}.dot{width:8px;height:8px;border-radius:50%;background:#59616d}.dot.ok{background:#49d17d;box-shadow:0 0 16px #49d17d88}.grid{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(320px,.9fr);gap:16px}' +
|
|
14
|
+
'.panel{background:#0f141a;border:1px solid #202935;border-radius:16px;overflow:hidden;box-shadow:0 14px 40px #0004}.panelHead{padding:16px 18px;border-bottom:1px solid #202935;display:flex;align-items:center;justify-content:space-between;gap:12px}.panelHead h2{font-size:14px;margin:0;letter-spacing:.02em}.panelBody{padding:14px 16px}' +
|
|
15
|
+
'.tabs{display:flex;gap:6px;padding:6px;background:#0a0e13;border-bottom:1px solid #202935}.tab{padding:8px 11px;border:0;border-radius:8px;background:transparent;color:#98a3b3}.tab.active{background:#18212b;color:#fff}.hidden{display:none!important}' +
|
|
16
|
+
'.task{padding:13px;border:1px solid #222d3a;border-radius:12px;margin-bottom:9px;background:#111820}.task:hover{border-color:#34465a}.taskTop{display:flex;justify-content:space-between;gap:12px}.taskTitle{font-size:14px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{font-size:11px;padding:4px 7px;border-radius:999px;background:#202a36;color:#b9c3d0}.badge.VERIFIED{background:#153522;color:#72e49c}.badge.FAILED,.badge.CANCELLED{background:#3c1d22;color:#ff969f}.badge.BLOCKED{background:#392e18;color:#ffd478}.badge.RUNNING{background:#182d4b;color:#8fb9ff}' +
|
|
17
|
+
'.actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}.small{padding:6px 8px;font-size:12px}.empty{padding:30px;text-align:center;color:#7e8998}.detail{white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;background:#090d12;border:1px solid #202935;border-radius:10px;padding:12px;max-height:480px;overflow:auto}' +
|
|
18
|
+
'.field{margin-bottom:11px}.field label{display:block;font-size:12px;color:#95a0af;margin-bottom:6px}.field textarea{width:100%;min-height:88px;resize:vertical}.field .input{width:100%;min-width:0}.two{display:grid;grid-template-columns:1fr 1fr;gap:10px}.event{padding:10px 0;border-bottom:1px solid #1c2530}.event:last-child{border:0}.eventTitle{font-size:12px;color:#d9e0e8}.eventMeta{font-size:11px;color:#798697;margin-top:4px}.device{padding:10px;border:1px solid #222d3a;border-radius:10px;margin-bottom:8px}.toast{position:fixed;right:20px;bottom:20px;max-width:420px;background:#151d27;border:1px solid #354458;border-radius:12px;padding:11px 14px;box-shadow:0 18px 50px #0008;z-index:10}.toast.bad{border-color:#704149;color:#ffc1c6}' +
|
|
19
|
+
'@media(max-width:900px){.grid{grid-template-columns:1fr}.top{align-items:flex-start;flex-direction:column}.auth{justify-content:flex-start;width:100%}.auth .input{flex:1}.shell{padding:14px}.two{grid-template-columns:1fr}}' +
|
|
20
|
+
'</style></head><body><main class="shell">' +
|
|
21
|
+
'<header class="top"><div class="brand"><div class="mark">M</div><div><h1>Mecord Control Center</h1><div class="muted">Local autonomous runtime · authoritative task state</div></div></div>' +
|
|
22
|
+
'<div class="auth"><input id="agentToken" class="input" type="password" autocomplete="off" placeholder="Agent token"><input id="recoveryToken" class="input" type="password" autocomplete="off" placeholder="Recovery token (approvals)"><button id="connect" class="btn primary">Connect</button><button id="refresh" class="btn">Refresh</button></div></header>' +
|
|
23
|
+
'<div class="grid"><section class="panel"><div class="panelHead"><h2>Operations</h2><div class="status"><span id="statusDot" class="dot"></span><span id="statusText" class="muted">Disconnected</span></div></div>' +
|
|
24
|
+
'<div class="tabs"><button class="tab active" data-tab="tasks">Tasks</button><button class="tab" data-tab="teams">Teams</button><button class="tab" data-tab="studio">Studio</button><button class="tab" data-tab="desired">Desired state</button><button class="tab" data-tab="goals">New goal</button><button class="tab" data-tab="activity">Activity</button><button class="tab" data-tab="devices">Devices</button></div>' +
|
|
25
|
+
'<div id="tasks" class="panelBody tabPage"><div id="taskList" class="empty">Connect to load tasks.</div></div>' +
|
|
26
|
+
'<div id="teams" class="panelBody tabPage hidden"><div id="teamList" class="empty">Connect to load Stage-4 missions.</div></div><div id="studio" class="panelBody tabPage hidden"><div class="two"><div class="field"><label>Compiled workflow ID</label><input id="studioWorkflowId" class="input" placeholder="Workflow UUID"></div><div class="field"><label>Values JSON</label><input id="studioValues" class="input" value="{}" spellcheck="false"></div></div><div class="actions"><button id="createStudioRun" class="btn primary">Create run</button></div><div id="studioList" class="empty">No Studio runs loaded.</div></div><div id="desired" class="panelBody tabPage hidden"><div class="field"><label>Desired-state contract JSON</label><textarea id="desiredJson" class="input" spellcheck="false" placeholder="Paste a desired-state contract JSON object"></textarea></div><div class="actions"><button id="createDesired" class="btn primary">Create contract</button></div><div id="desiredList" class="empty">No desired-state contracts loaded.</div></div>' +
|
|
27
|
+
'<div id="goals" class="panelBody tabPage hidden"><div class="field"><label>Objective</label><input id="objective" class="input" placeholder="What should Mecord accomplish?"></div><div class="field"><label>Success conditions · one per line</label><textarea id="success" class="input" placeholder="Build passes Requested state is verified"></textarea></div><div class="field"><label>Goal JSON</label><textarea id="goalJson" class="input" spellcheck="false" placeholder=\'{"kind":"project-quality-gate","root":"C:\\\\path","checks":["lint","test","build"],"requireAll":true}\'></textarea></div><div class="actions"><button id="submitGoal" class="btn primary">Submit & run</button><button id="queueGoal" class="btn">Queue only</button></div></div>' +
|
|
28
|
+
'<div id="activity" class="panelBody tabPage hidden"><div id="activityList" class="empty">No activity loaded.</div></div>' +
|
|
29
|
+
'<div id="devices" class="panelBody tabPage hidden"><div id="deviceList" class="empty">No devices loaded.</div></div></section>' +
|
|
30
|
+
'<aside class="panel"><div class="panelHead"><h2>Detail</h2><span id="selectedState" class="badge">—</span></div><div class="panelBody"><div id="taskDetail" class="empty">Select a task or team mission to inspect its durable state.</div></div></aside></div></main><div id="toast" class="toast hidden"></div>' +
|
|
31
|
+
'<script nonce="' + safeNonce + '">' +
|
|
32
|
+
'(()=>{"use strict";let token="",selected="",selectedKind="task",poll=null;const $=id=>document.getElementById(id);' +
|
|
33
|
+
'function toast(message,bad){const n=$("toast");n.textContent=message;n.className="toast"+(bad?" bad":"");clearTimeout(n._t);n._t=setTimeout(()=>n.classList.add("hidden"),4200)}' +
|
|
34
|
+
'async function api(path,init){if(!token)throw new Error("Connect with the agent token first.");const opts=Object.assign({},init||{});opts.headers=Object.assign({},opts.headers||{},{"authorization":"Bearer "+token});if(opts.body&&!opts.headers["content-type"])opts.headers["content-type"]="application/json";const res=await fetch(path,opts);let body={};try{body=await res.json()}catch{}if(!res.ok)throw new Error((body.error&&body.error.code?body.error.code+": ":"")+(body.error&&body.error.message?body.error.message:"Request failed"));return body}' +
|
|
35
|
+
'function node(tag,cls,text){const n=document.createElement(tag);if(cls)n.className=cls;if(text!==undefined)n.textContent=String(text);return n}' +
|
|
36
|
+
'function button(text,cls,fn){const b=node("button","btn small "+(cls||""),text);b.type="button";b.addEventListener("click",e=>{e.stopPropagation();fn()});return b}' +
|
|
37
|
+
'function stateBadge(state){return node("span","badge "+state,state)}' +
|
|
38
|
+
'async function loadTasks(){const data=await api("/v1/tasks?limit=100");const box=$("taskList");box.replaceChildren();if(!data.tasks||!data.tasks.length){box.append(node("div","empty","No tasks yet."));return}for(const task of data.tasks){const card=node("div","task");const top=node("div","taskTop");const title=node("div","taskTitle",task.userObjective||task.id);top.append(title,stateBadge(task.state));card.append(top,node("div","muted",(task.id||"")+" · "+(task.updatedAt||"")));const actions=node("div","actions");if(task.state==="PENDING")actions.append(button("Run","primary",()=>control(task.id,"run")));if(task.state==="RUNNING")actions.append(button("Pause","",()=>control(task.id,"pause")),button("Cancel","danger",()=>control(task.id,"cancel")));if(task.state==="PAUSED")actions.append(button("Resume","primary",()=>control(task.id,"resume")),button("Cancel","danger",()=>control(task.id,"cancel")));if(task.state==="BLOCKED")actions.append(button("Review approval","warn",()=>selectTask(task.id)));if(!["VERIFIED","FAILED","CANCELLED"].includes(task.state))actions.append(button("Cancel","danger",()=>control(task.id,"cancel")));card.append(actions);card.addEventListener("click",()=>selectTask(task.id));box.append(card)}}' +
|
|
39
|
+
'async function loadTeams(){const data=await api("/v1/teams?limit=100");const box=$("teamList");box.replaceChildren();if(!data.teams||!data.teams.length){box.append(node("div","empty","No Stage-4 missions yet."));return}for(const team of data.teams){const card=node("div","task");const top=node("div","taskTop");top.append(node("div","taskTitle",team.objective||team.id),stateBadge(team.state));card.append(top,node("div","muted",(team.id||"")+" · "+(team.updatedAt||"")));const actions=node("div","actions");if(team.state==="PENDING")actions.append(button("Start","primary",()=>teamControl(team.id,"start")));if(team.state==="RUNNING"||team.state==="BLOCKED")actions.append(button("Pause","",()=>teamControl(team.id,"pause")));if(team.state==="PAUSED"||team.state==="BLOCKED")actions.append(button("Resume","primary",()=>teamControl(team.id,"resume")));if(!["VERIFIED","FAILED","CANCELLED"].includes(team.state))actions.append(button("Cancel","danger",()=>teamControl(team.id,"cancel")));card.append(actions);card.addEventListener("click",()=>selectTeam(team.id));box.append(card)}}' +
|
|
40
|
+
'async function selectTeam(id){selected=id;selectedKind="team";const data=await api("/v1/teams/"+encodeURIComponent(id));const team=data.mission;$("selectedState").textContent=team.state;$("selectedState").className="badge "+team.state;const wrap=node("div");const actions=node("div","actions");if(team.state==="PENDING")actions.append(button("Start","primary",()=>teamControl(id,"start")));if(team.state==="RUNNING"||team.state==="BLOCKED")actions.append(button("Pause","",()=>teamControl(id,"pause")));if(team.state==="PAUSED"||team.state==="BLOCKED")actions.append(button("Resume","primary",()=>teamControl(id,"resume")));if(!["VERIFIED","FAILED","CANCELLED"].includes(team.state))actions.append(button("Cancel","danger",()=>teamControl(id,"cancel")));wrap.append(actions,node("pre","detail",JSON.stringify(team,null,2)));$("taskDetail").replaceChildren(wrap)}' +
|
|
41
|
+
'async function teamControl(id,op){try{await api("/v1/teams/"+encodeURIComponent(id)+"/"+op,{method:"POST",body:"{}"});await refresh();if(selectedKind==="team"&&selected===id)await selectTeam(id)}catch(e){toast(e.message,true)}}' +
|
|
42
|
+
'async function selectTask(id){selected=id;selectedKind="task";const data=await api("/v1/tasks/"+encodeURIComponent(id));const task=data.task;$("selectedState").textContent=task.state;$("selectedState").className="badge "+task.state;const wrap=node("div");const actions=node("div","actions");if(task.state==="PENDING")actions.append(button("Run","primary",()=>control(id,"run")));if(task.state==="RUNNING")actions.append(button("Pause","",()=>control(id,"pause")));if(task.state==="PAUSED")actions.append(button("Resume","primary",()=>control(id,"resume")));if(task.state==="BLOCKED"){const record=(task.execution&&task.execution.records||[]).find(r=>r.state==="BLOCKED");if(record)actions.append(button("Approve & resume","warn",()=>approve(id,record.actionId)));}if(!["VERIFIED","FAILED","CANCELLED"].includes(task.state))actions.append(button("Cancel","danger",()=>control(id,"cancel")));wrap.append(actions);const pre=node("pre","detail",JSON.stringify(task,null,2));wrap.append(pre);$("taskDetail").replaceChildren(wrap)}' +
|
|
43
|
+
'async function control(id,op){try{await api("/v1/tasks/"+encodeURIComponent(id)+"/"+op,{method:"POST",body:"{}"});await refresh();if(selected===id)await selectTask(id)}catch(e){toast(e.message,true)}}' +
|
|
44
|
+
'async function approve(id,actionId){const recovery=$("recoveryToken").value.trim();if(!recovery){toast("Recovery token is required for explicit approval.",true);return}try{await api("/v1/tasks/"+encodeURIComponent(id)+"/resume",{method:"POST",headers:{"x-operator-recovery-token":recovery},body:JSON.stringify({approvedActionId:actionId})});await refresh();await selectTask(id)}catch(e){toast(e.message,true)}}' +
|
|
45
|
+
'async function loadStudioRuns(){const data=await api("/v1/studio/runs?limit=100");const box=$("studioList");box.replaceChildren();if(!data.runs||!data.runs.length){box.append(node("div","empty","No Studio runs yet."));return}for(const run of data.runs){const card=node("div","task");const top=node("div","taskTop");top.append(node("div","taskTitle","Workflow "+(run.workflowId||"")),stateBadge(run.state));card.append(top,node("div","muted",(run.id||"")+" · "+(run.updatedAt||"")));const actions=node("div","actions");if(run.state==="PENDING")actions.append(button("Execute next","primary",()=>studioControl(run.id,"execute")));if(!["VERIFIED","FAILED","CANCELLED","RUNNING"].includes(run.state))actions.append(button("Cancel","danger",()=>studioControl(run.id,"cancel")));card.append(actions);card.addEventListener("click",()=>selectStudioRun(run.id));box.append(card)}}' +
|
|
46
|
+
'async function selectStudioRun(id){selected=id;selectedKind="studio";const data=await api("/v1/studio/runs/"+encodeURIComponent(id));const run=data.run;$("selectedState").textContent=run.state;$("selectedState").className="badge "+run.state;const wrap=node("div");const actions=node("div","actions");if(run.state==="PENDING")actions.append(button("Execute next","primary",()=>studioControl(id,"execute")));if(!["VERIFIED","FAILED","CANCELLED","RUNNING"].includes(run.state))actions.append(button("Cancel","danger",()=>studioControl(id,"cancel")));wrap.append(actions,node("pre","detail",JSON.stringify(run,null,2)));$("taskDetail").replaceChildren(wrap)}' +
|
|
47
|
+
'async function studioControl(id,op){try{await api("/v1/studio/runs/"+encodeURIComponent(id)+"/"+op,{method:"POST",body:op==="execute"?JSON.stringify({maxSteps:1}):"{}"});await refresh();if(selectedKind==="studio"&&selected===id)await selectStudioRun(id)}catch(e){toast(e.message,true)}}' +
|
|
48
|
+
'async function createStudioRun(){try{const workflowId=$("studioWorkflowId").value.trim();if(!workflowId)throw new Error("Compiled workflow ID is required.");const values=JSON.parse($("studioValues").value||"{}");const data=await api("/v1/studio/workflows/"+encodeURIComponent(workflowId)+"/run",{method:"POST",body:JSON.stringify({values})});toast("Studio run created.");await loadStudioRuns();if(data.run&&data.run.id)await selectStudioRun(data.run.id)}catch(e){toast(e.message,true)}}' +
|
|
49
|
+
'async function loadDesired(){const data=await api("/v1/desired-state?limit=100");const box=$("desiredList");box.replaceChildren();if(!data.contracts||!data.contracts.length){box.append(node("div","empty","No desired-state contracts yet."));return}for(const item of data.contracts){const card=node("div","task");const top=node("div","taskTop");top.append(node("div","taskTitle",item.name||item.id),stateBadge(item.status));card.append(top,node("div","muted",(item.id||"")+" · "+(item.updatedAt||"")));const actions=node("div","actions");if(item.status==="PAUSED")actions.append(button("Resume","primary",()=>desiredControl(item.id,"resume")));else actions.append(button("Reconcile","primary",()=>desiredControl(item.id,"reconcile")),button("Pause","",()=>desiredControl(item.id,"pause")));card.append(actions);card.addEventListener("click",()=>selectDesired(item.id));box.append(card)}}' +
|
|
50
|
+
'async function selectDesired(id){selected=id;selectedKind="desired";const data=await api("/v1/desired-state/"+encodeURIComponent(id));const item=data.contract;$("selectedState").textContent=item.status;$("selectedState").className="badge "+item.status;const wrap=node("div");const actions=node("div","actions");if(item.status==="PAUSED")actions.append(button("Resume","primary",()=>desiredControl(id,"resume")));else actions.append(button("Reconcile","primary",()=>desiredControl(id,"reconcile")),button("Pause","",()=>desiredControl(id,"pause")));wrap.append(actions,node("pre","detail",JSON.stringify(item,null,2)));$("taskDetail").replaceChildren(wrap)}' +
|
|
51
|
+
'async function desiredControl(id,op){try{await api("/v1/desired-state/"+encodeURIComponent(id)+"/"+op,{method:"POST",body:"{}"});await refresh();if(selectedKind==="desired"&&selected===id)await selectDesired(id)}catch(e){toast(e.message,true)}}' +
|
|
52
|
+
'async function createDesired(){try{const body=JSON.parse($("desiredJson").value);const data=await api("/v1/desired-state",{method:"POST",body:JSON.stringify(body)});toast("Desired-state contract created.");await loadDesired();if(data.contract&&data.contract.id)await selectDesired(data.contract.id)}catch(e){toast(e.message,true)}}' +
|
|
53
|
+
'async function loadActivity(){const data=await api("/v1/activity?limit=100");const box=$("activityList");box.replaceChildren();if(!data.events||!data.events.length){box.append(node("div","empty","No activity recorded."));return}for(const ev of data.events.slice().reverse()){const row=node("div","event");row.append(node("div","eventTitle",(ev.capability||ev.type||"event")+" · "+(ev.result||ev.status||"")),node("div","eventMeta",(ev.timestamp||ev.at||"")+" "+(ev.target||"")));box.append(row)}}' +
|
|
54
|
+
'async function loadDevices(){const data=await api("/v1/devices");const box=$("deviceList");box.replaceChildren();if(data.local){const d=node("div","device");d.append(node("div","taskTitle",data.local.deviceName||"Local device"),node("div","muted",data.local.deviceId||""));box.append(d)}for(const peer of data.peers||[]){const d=node("div","device");d.append(node("div","taskTitle",peer.deviceName||"Paired device"),node("div","muted",(peer.status||"")+" · "+(peer.deviceId||"")));box.append(d)}if(!data.local&&!(data.peers||[]).length)box.append(node("div","empty","No device identity configured."))}' +
|
|
55
|
+
'async function refresh(){try{await Promise.all([loadTasks(),loadTeams(),loadStudioRuns(),loadDesired(),loadActivity(),loadDevices()]);$("statusDot").className="dot ok";$("statusText").textContent="Connected";if(selected){if(selectedKind==="team")await selectTeam(selected);else if(selectedKind==="studio")await selectStudioRun(selected);else if(selectedKind==="desired")await selectDesired(selected);else await selectTask(selected)}}catch(e){$("statusDot").className="dot";$("statusText").textContent="Disconnected";toast(e.message,true)}}' +
|
|
56
|
+
'async function submit(run){try{const objective=$("objective").value.trim();if(!objective)throw new Error("Objective is required.");const goal=JSON.parse($("goalJson").value);const success=$("success").value.split(/\\r?\\n/).map(x=>x.trim()).filter(Boolean);if(!success.length)throw new Error("At least one success condition is required.");const data=await api("/v1/goals",{method:"POST",body:JSON.stringify({objective,successConditions:success,goal,run})});toast(run?"Goal submitted and execution started.":"Goal queued.");await refresh();if(data.task&&data.task.id)await selectTask(data.task.id)}catch(e){toast(e.message,true)}}' +
|
|
57
|
+
'$("connect").addEventListener("click",()=>{token=$("agentToken").value.trim();$("agentToken").value="";if(!token){toast("Agent token is required.",true);return}refresh();if(poll)clearInterval(poll);poll=setInterval(()=>{if(token)Promise.all([loadTasks(),loadTeams(),loadStudioRuns(),loadDesired()]).catch(()=>{})},2500)});$("refresh").addEventListener("click",refresh);$("submitGoal").addEventListener("click",()=>submit(true));$("queueGoal").addEventListener("click",()=>submit(false));$("createStudioRun").addEventListener("click",createStudioRun);$("createDesired").addEventListener("click",createDesired);' +
|
|
58
|
+
'for(const tab of document.querySelectorAll(".tab"))tab.addEventListener("click",()=>{for(const t of document.querySelectorAll(".tab"))t.classList.toggle("active",t===tab);for(const page of document.querySelectorAll(".tabPage"))page.classList.toggle("hidden",page.id!==tab.dataset.tab)});' +
|
|
59
|
+
'window.addEventListener("beforeunload",()=>{token="";$("recoveryToken").value=""});' +
|
|
60
|
+
'})();</script></body></html>';
|
|
61
|
+
}
|
|
@@ -18,7 +18,26 @@ import { OperatorError } from '../../../src/core/errors.js';
|
|
|
18
18
|
import { RelayEnrollmentClient } from './relay-enrollment.js';
|
|
19
19
|
import { DEVELOPER_RELAY_CAPABILITIES } from '../../../src/core/developer-relay-surface.js';
|
|
20
20
|
import { TaskOrchestrator } from '../../../src/core/task-orchestrator.js';
|
|
21
|
+
import { TeamCoordinator } from '../../../src/core/team-coordinator.js';
|
|
22
|
+
import { ProcedureMemoryStore } from '../../../src/core/procedure-memory.js';
|
|
23
|
+
import { WorldModelStore } from '../../../src/core/world-model.js';
|
|
24
|
+
import { DeviceRoutingStore } from '../../../src/core/device-routing.js';
|
|
25
|
+
import { DevicePoolScheduler } from '../../../src/core/device-pool.js';
|
|
26
|
+
import { ExecutionOptimizerStore } from '../../../src/core/execution-optimizer.js';
|
|
27
|
+
import { OrganizationCoordinator } from '../../../src/core/organization-coordinator.js';
|
|
28
|
+
import { DigitalOperationsLayer } from '../../../src/core/digital-operations.js';
|
|
21
29
|
import { evidence } from '../../../src/core/evidence.js';
|
|
30
|
+
import { ResourceLeaseStore } from '../../../src/core/resource-leases.js';
|
|
31
|
+
import { TeachModeStore } from '../../../src/core/studio-teach.js';
|
|
32
|
+
import { DesiredStateController } from '../../../src/core/desired-state.js';
|
|
33
|
+
import { DesiredStateReconciler } from '../../../src/core/desired-state-reconciler.js';
|
|
34
|
+
import { DurableEventRuntime } from '../../../src/core/event-runtime.js';
|
|
35
|
+
import { DurableEventTicker } from '../../../src/core/event-ticker.js';
|
|
36
|
+
import { PerceptionGraphStore } from '../../../src/core/perception-graph.js';
|
|
37
|
+
import { publishPerceptionFromActionResult } from '../../../src/core/perception-publication.js';
|
|
38
|
+
import { StudioWorkflowExecutor } from '../../../src/core/studio-executor.js';
|
|
39
|
+
import { SemanticCheckpointManager } from '../../../src/core/semantic-checkpoint.js';
|
|
40
|
+
import { EnterprisePolicyStore } from '../../../src/core/enterprise-policy.js';
|
|
22
41
|
|
|
23
42
|
const allowedRoots = (process.env.OPERATOR_ALLOWED_ROOTS ?? process.cwd())
|
|
24
43
|
.split(path.delimiter)
|
|
@@ -51,13 +70,36 @@ if (recoveryToken !== undefined && recoveryToken.length < 32) {
|
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
const stateDir = path.resolve(process.env.OPERATOR_STATE_DIR ?? path.join(os.homedir(), '.operator'));
|
|
73
|
+
const permissions = {
|
|
74
|
+
allowedCapabilities: ['computer.inspect', 'project.inspect', 'project.command.*', 'project.transaction.*', 'docker.*', 'compute.run', 'postgres.*', 'vscode.*', 'file.*', 'git.*', 'terminal.execute', 'terminal.session', 'process.inspect', 'process.manage', 'browser.inspect', 'browser.navigate', 'browser.interact', 'app.inspect', 'app.operate', 'visual.capture', 'input.operate', 'perception.*'],
|
|
75
|
+
allowedRoots,
|
|
76
|
+
allowExternalWrites: false,
|
|
77
|
+
allowSystemChanges: false,
|
|
78
|
+
allowDestructive: false
|
|
79
|
+
};
|
|
54
80
|
const emergencyStop = new EmergencyStopStore(stateDir);
|
|
55
81
|
const approvals = new ApprovalStore(stateDir);
|
|
56
82
|
const sessionApprovals = new SessionApprovalStore();
|
|
57
83
|
const audit = new AuditLog(stateDir);
|
|
58
84
|
const tasks = new TaskStore(stateDir);
|
|
85
|
+
const resourceLeases = new ResourceLeaseStore(stateDir);
|
|
86
|
+
const teams = new TeamCoordinator(stateDir);
|
|
87
|
+
const procedures = new ProcedureMemoryStore(stateDir);
|
|
88
|
+
const world = new WorldModelStore(stateDir);
|
|
89
|
+
const perception = new PerceptionGraphStore(stateDir);
|
|
90
|
+
const optimizer = new ExecutionOptimizerStore(stateDir);
|
|
59
91
|
const deviceIdentity = new DeviceIdentityStore(stateDir);
|
|
60
92
|
const deviceRegistry = new DeviceRegistryStore(stateDir);
|
|
93
|
+
const semanticMigration = new SemanticCheckpointManager(stateDir, {
|
|
94
|
+
identity: deviceIdentity,
|
|
95
|
+
registry: deviceRegistry
|
|
96
|
+
});
|
|
97
|
+
const deviceRouting = new DeviceRoutingStore(stateDir, deviceRegistry);
|
|
98
|
+
const devicePool = new DevicePoolScheduler(stateDir, deviceRegistry, deviceRouting);
|
|
99
|
+
const organizations = new OrganizationCoordinator(stateDir, teams);
|
|
100
|
+
const enterprisePolicy = new EnterprisePolicyStore(stateDir);
|
|
101
|
+
const teachMode = new TeachModeStore(stateDir);
|
|
102
|
+
const events = new DurableEventRuntime(stateDir);
|
|
61
103
|
const privacy = new LocalPrivacyDataStore(stateDir);
|
|
62
104
|
const browserAutoLaunch = process.env.OPERATOR_BROWSER_AUTO_LAUNCH !== '0';
|
|
63
105
|
const relayUrl = process.env.OPERATOR_RELAY_URL?.trim();
|
|
@@ -84,6 +126,8 @@ const runtime = createRuntime({
|
|
|
84
126
|
terminalAllowedExecutables,
|
|
85
127
|
projectCommandRegistryPath: process.env.OPERATOR_PROJECT_COMMAND_REGISTRY,
|
|
86
128
|
dockerExecutable: process.env.OPERATOR_DOCKER_PATH,
|
|
129
|
+
computeJavascriptImage: process.env.OPERATOR_COMPUTE_JS_IMAGE,
|
|
130
|
+
computePythonImage: process.env.OPERATOR_COMPUTE_PY_IMAGE,
|
|
87
131
|
postgresProfileRegistryPath: process.env.OPERATOR_POSTGRES_PROFILE_REGISTRY,
|
|
88
132
|
psqlExecutable: process.env.OPERATOR_PSQL_PATH,
|
|
89
133
|
vscodeExecutable: process.env.OPERATOR_VSCODE_PATH,
|
|
@@ -93,7 +137,42 @@ const runtime = createRuntime({
|
|
|
93
137
|
browserPath: process.env.OPERATOR_BROWSER_PATH,
|
|
94
138
|
browserDataDir: process.env.OPERATOR_BROWSER_DATA_DIR,
|
|
95
139
|
windowsUiaPath: process.env.OPERATOR_WINDOWS_UIA_PATH,
|
|
96
|
-
windowsPathLeasePath: process.env.OPERATOR_WINDOWS_PATH_LEASE_PATH
|
|
140
|
+
windowsPathLeasePath: process.env.OPERATOR_WINDOWS_PATH_LEASE_PATH,
|
|
141
|
+
perception
|
|
142
|
+
});
|
|
143
|
+
const studioExecutor = new StudioWorkflowExecutor(stateDir, {
|
|
144
|
+
teach: teachMode,
|
|
145
|
+
runtime,
|
|
146
|
+
leases: resourceLeases,
|
|
147
|
+
permissions
|
|
148
|
+
});
|
|
149
|
+
const recoveredStudioRuns = await studioExecutor.recoverInterrupted();
|
|
150
|
+
const operationCapabilities = await runtime.supportedCapabilities(permissions.allowedCapabilities);
|
|
151
|
+
const operations = new DigitalOperationsLayer(stateDir, {
|
|
152
|
+
procedures,
|
|
153
|
+
world,
|
|
154
|
+
devices: devicePool,
|
|
155
|
+
optimizer,
|
|
156
|
+
teams,
|
|
157
|
+
organizations,
|
|
158
|
+
availableCapabilities: operationCapabilities
|
|
159
|
+
});
|
|
160
|
+
const desiredState = new DesiredStateController(stateDir, { world, operations });
|
|
161
|
+
const desiredStateIntervalMs = Number(process.env.OPERATOR_DESIRED_STATE_INTERVAL_MS ?? 60_000);
|
|
162
|
+
if (!Number.isSafeInteger(desiredStateIntervalMs) || desiredStateIntervalMs < 1_000 || desiredStateIntervalMs > 24 * 60 * 60_000) {
|
|
163
|
+
throw new OperatorError('DESIRED_STATE_INTERVAL_INVALID', 'OPERATOR_DESIRED_STATE_INTERVAL_MS must be an integer from 1000 to 86400000.');
|
|
164
|
+
}
|
|
165
|
+
const desiredStateReconciler = new DesiredStateReconciler(desiredState, {
|
|
166
|
+
intervalMs: desiredStateIntervalMs,
|
|
167
|
+
onError: (error) => console.error(`[operator] desired-state reconcile tick failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
168
|
+
});
|
|
169
|
+
const eventTickIntervalMs = Number(process.env.OPERATOR_EVENT_TICK_INTERVAL_MS ?? 1_000);
|
|
170
|
+
if (!Number.isSafeInteger(eventTickIntervalMs) || eventTickIntervalMs < 250 || eventTickIntervalMs > 60_000) {
|
|
171
|
+
throw new OperatorError('EVENT_TICK_INTERVAL_INVALID', 'OPERATOR_EVENT_TICK_INTERVAL_MS must be an integer from 250 to 60000.');
|
|
172
|
+
}
|
|
173
|
+
const eventTicker = new DurableEventTicker(events, {
|
|
174
|
+
intervalMs: eventTickIntervalMs,
|
|
175
|
+
onError: (error) => console.error(`[operator] event tick failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
97
176
|
});
|
|
98
177
|
let relayRunner = null;
|
|
99
178
|
let relayRun = null;
|
|
@@ -111,7 +190,7 @@ async function failRequiredRelay(error ) {
|
|
|
111
190
|
const message = error instanceof Error ? error.message : String(error);
|
|
112
191
|
console.error(`[operator] required relay failed: ${message}`);
|
|
113
192
|
stopRelay();
|
|
114
|
-
await Promise.allSettled([agent.close(), runtime.close()]);
|
|
193
|
+
await Promise.allSettled([desiredStateReconciler.stop(), eventTicker.stop(), agent.close(), runtime.close()]);
|
|
115
194
|
process.exitCode = 1;
|
|
116
195
|
setImmediate(() => process.exit(1));
|
|
117
196
|
}
|
|
@@ -153,6 +232,15 @@ function startRelay() {
|
|
|
153
232
|
localAgentBaseUrl,
|
|
154
233
|
agentToken: token,
|
|
155
234
|
getSupportedCapabilities: () => runtime.supportedCapabilities(DEVELOPER_RELAY_CAPABILITIES),
|
|
235
|
+
onStatus: (status) => {
|
|
236
|
+
if (status.state === 'socket-connected') {
|
|
237
|
+
console.error('[operator] relay socket connected; authenticating');
|
|
238
|
+
} else if (status.state === 'authenticated-ready') {
|
|
239
|
+
console.error(`[operator] relay authenticated and ready (${status.capabilityCount} capabilities)`);
|
|
240
|
+
} else {
|
|
241
|
+
console.error(`[operator] relay reconnect scheduled (${status.code}, ${status.delayMs}ms)`);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
156
244
|
allowLoopbackInsecure: relayAllowInsecureLoopback
|
|
157
245
|
});
|
|
158
246
|
const runner = relayRunner;
|
|
@@ -193,18 +281,12 @@ async function resetLocalDevice() {
|
|
|
193
281
|
return await coordinator.reset();
|
|
194
282
|
}
|
|
195
283
|
|
|
196
|
-
const permissions = {
|
|
197
|
-
allowedCapabilities: ['computer.inspect', 'project.inspect', 'project.command.*', 'project.transaction.*', 'docker.*', 'postgres.*', 'vscode.*', 'file.*', 'git.*', 'terminal.execute', 'browser.inspect', 'browser.navigate', 'browser.interact', 'app.inspect', 'app.operate'],
|
|
198
|
-
allowedRoots,
|
|
199
|
-
allowExternalWrites: false,
|
|
200
|
-
allowSystemChanges: false,
|
|
201
|
-
allowDestructive: false
|
|
202
|
-
};
|
|
203
284
|
const taskOrchestrator = new TaskOrchestrator({
|
|
204
285
|
runtime,
|
|
205
286
|
store: tasks,
|
|
206
287
|
permissions,
|
|
207
|
-
|
|
288
|
+
resourceLeases,
|
|
289
|
+
executeAction: async (action, actionPermissions, context) => {
|
|
208
290
|
if ((await emergencyStop.status()).engaged) {
|
|
209
291
|
return {
|
|
210
292
|
ok: false,
|
|
@@ -215,14 +297,33 @@ const taskOrchestrator = new TaskOrchestrator({
|
|
|
215
297
|
durationMs: 0
|
|
216
298
|
};
|
|
217
299
|
}
|
|
218
|
-
const result = await runtime.execute(action, actionPermissions);
|
|
300
|
+
const result = await runtime.execute(action, actionPermissions, context);
|
|
301
|
+
try {
|
|
302
|
+
await publishPerceptionFromActionResult(perception, action, result);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
await audit.append({
|
|
305
|
+
...(action.taskId ? { traceId: action.taskId, taskId: action.taskId } : {}),
|
|
306
|
+
actionId: action.id,
|
|
307
|
+
providerId: 'perception.graph',
|
|
308
|
+
capability: 'perception.publish',
|
|
309
|
+
result: 'failure',
|
|
310
|
+
risk: 'write',
|
|
311
|
+
details: { code: typeof (error )?.code === 'string' ? (error ).code : 'PERCEPTION_PUBLICATION_FAILED' }
|
|
312
|
+
});
|
|
313
|
+
}
|
|
219
314
|
await audit.append({
|
|
220
|
-
taskId: action.taskId,
|
|
315
|
+
...(action.taskId ? { traceId: action.taskId, taskId: action.taskId } : {}),
|
|
316
|
+
actionId: action.id,
|
|
317
|
+
providerId: result.provider,
|
|
221
318
|
capability: action.capability,
|
|
222
319
|
target: action.target,
|
|
223
320
|
result: result.ok ? 'success' : result.provider === 'policy' ? 'blocked' : 'failure',
|
|
224
321
|
risk: action.risk,
|
|
225
|
-
details: {
|
|
322
|
+
details: {
|
|
323
|
+
durationMs: result.durationMs,
|
|
324
|
+
errorCode: result.error?.code,
|
|
325
|
+
sideEffectState: result.error?.sideEffectState
|
|
326
|
+
}
|
|
226
327
|
});
|
|
227
328
|
return result;
|
|
228
329
|
}
|
|
@@ -238,6 +339,20 @@ const agent = createLocalAgentServer({
|
|
|
238
339
|
audit,
|
|
239
340
|
tasks,
|
|
240
341
|
taskOrchestrator,
|
|
342
|
+
teams,
|
|
343
|
+
procedures,
|
|
344
|
+
world,
|
|
345
|
+
devicePool,
|
|
346
|
+
optimizer,
|
|
347
|
+
organizations,
|
|
348
|
+
operations,
|
|
349
|
+
events,
|
|
350
|
+
perception,
|
|
351
|
+
teachMode,
|
|
352
|
+
studioExecutor,
|
|
353
|
+
semanticMigration,
|
|
354
|
+
enterprisePolicy,
|
|
355
|
+
desiredState,
|
|
241
356
|
deviceIdentity,
|
|
242
357
|
deviceRegistry,
|
|
243
358
|
privacy,
|
|
@@ -257,6 +372,14 @@ const agent = createLocalAgentServer({
|
|
|
257
372
|
postgresConfigured: Boolean(process.env.OPERATOR_POSTGRES_PROFILE_REGISTRY),
|
|
258
373
|
vscodeConfigured: Boolean(process.env.OPERATOR_VSCODE_PATH),
|
|
259
374
|
windowsUiaConfigured: Boolean(process.env.OPERATOR_WINDOWS_UIA_PATH),
|
|
375
|
+
studioWorkflowExecutorConfigured: true,
|
|
376
|
+
semanticMigrationConfigured: true,
|
|
377
|
+
enterprisePolicyConfigured: await enterprisePolicy.isConfigured(),
|
|
378
|
+
recoveredStudioRunCount: recoveredStudioRuns,
|
|
379
|
+
desiredStateReconcilerConfigured: true,
|
|
380
|
+
desiredStateIntervalMs,
|
|
381
|
+
eventRuntimeConfigured: true,
|
|
382
|
+
eventTickIntervalMs,
|
|
260
383
|
relayConfigured: Boolean(relayUrl),
|
|
261
384
|
relayResultConfigured: Boolean(relayResultUrl),
|
|
262
385
|
relayTokenFileConfigured: Boolean(relayUrl),
|
|
@@ -280,6 +403,11 @@ console.error(`[operator] protected state directory: ${stateDir}`);
|
|
|
280
403
|
console.error(`[operator] recovery API: ${recoveryToken ? 'configured' : 'disabled until OPERATOR_RECOVERY_TOKEN is set'}`);
|
|
281
404
|
console.error(`[operator] generic terminal: ${terminalAllowedExecutables.length ? 'explicit allowlist configured' : 'disabled by default'}`);
|
|
282
405
|
console.error(`[operator] relay: ${relayUrl ? 'configured' : 'disabled'}`);
|
|
406
|
+
console.error(`[operator] studio workflow recovery: ${recoveredStudioRuns} interrupted run(s) reconciled`);
|
|
407
|
+
console.error(`[operator] desired-state reconciler: every ${desiredStateIntervalMs}ms`);
|
|
408
|
+
console.error(`[operator] durable event ticker: every ${eventTickIntervalMs}ms`);
|
|
409
|
+
desiredStateReconciler.start();
|
|
410
|
+
eventTicker.start();
|
|
283
411
|
if (relayUrl) {
|
|
284
412
|
const relayCapabilities = await runtime.supportedCapabilities(DEVELOPER_RELAY_CAPABILITIES);
|
|
285
413
|
console.error(`[operator] relay capabilities: ${relayCapabilities.join(', ') || 'none'}`);
|
|
@@ -306,7 +434,7 @@ for (const signal of ['SIGINT', 'SIGTERM'] ) {
|
|
|
306
434
|
if (shuttingDown) return;
|
|
307
435
|
shuttingDown = true;
|
|
308
436
|
stopRelay();
|
|
309
|
-
await Promise.allSettled([relayRun, agent.close(), runtime.close()].filter(Boolean) );
|
|
437
|
+
await Promise.allSettled([relayRun, desiredStateReconciler.stop(), eventTicker.stop(), agent.close(), runtime.close()].filter(Boolean) );
|
|
310
438
|
process.exit(0);
|
|
311
439
|
});
|
|
312
440
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { OperatorError } from '../../../src/core/errors.js';
|
|
6
|
+
import { PathScope } from '../../../src/capabilities/path-scope.js';
|
|
7
|
+
import { semanticCheckpointDigest, } from '../../../src/core/semantic-checkpoint.js';
|
|
8
|
+
import { worldValueDigest, } from '../../../src/core/world-model.js';
|
|
9
|
+
|
|
10
|
+
const CAPABILITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
11
|
+
|
|
12
|
+
export function migrationAuthorityDigest(input
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
) {
|
|
16
|
+
const requiredCapabilities = exactCapabilities(input.requiredCapabilities);
|
|
17
|
+
const resourceKeys = normalizedResourceKeys(input.resourceKeys);
|
|
18
|
+
return semanticCheckpointDigest({
|
|
19
|
+
version: 1,
|
|
20
|
+
requiredCapabilities,
|
|
21
|
+
resourceKeys
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertMigrationCapabilities(input
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
) {
|
|
30
|
+
const required = exactCapabilities(input.requiredCapabilities);
|
|
31
|
+
const supported = new Set(input.supportedCapabilities);
|
|
32
|
+
for (const capability of required) {
|
|
33
|
+
if (!profileAllows(capability, input.permissions.allowedCapabilities)) {
|
|
34
|
+
throw new OperatorError('SEMANTIC_MIGRATION_CAPABILITY_DENIED', `Capability ${capability} is outside local authority.`);
|
|
35
|
+
}
|
|
36
|
+
if (!supported.has(capability)) {
|
|
37
|
+
throw new OperatorError('SEMANTIC_MIGRATION_CAPABILITY_UNAVAILABLE', `Capability ${capability} is not supported on this device.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return required;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function verifyMigrationResourceKey(
|
|
44
|
+
resourceKeyInput ,
|
|
45
|
+
permissions ,
|
|
46
|
+
requiredCapabilitiesInput
|
|
47
|
+
) {
|
|
48
|
+
const resourceKey = bounded(resourceKeyInput, 1024, 'resourceKey');
|
|
49
|
+
const required = exactCapabilities(requiredCapabilitiesInput);
|
|
50
|
+
const allowFamily = (prefixes ) => required.some((capability) => prefixes.some((prefix) => capability.startsWith(prefix)))
|
|
51
|
+
&& required.every((capability) => profileAllows(capability, permissions.allowedCapabilities));
|
|
52
|
+
|
|
53
|
+
const physical = physicalResource(resourceKey);
|
|
54
|
+
if (physical) {
|
|
55
|
+
if (permissions.allowedRoots.length < 1 || !allowFamily(physical.capabilityPrefixes)) return false;
|
|
56
|
+
try {
|
|
57
|
+
const scope = new PathScope(permissions.allowedRoots);
|
|
58
|
+
await scope.resolveExisting(physical.path);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (resourceKey.startsWith('browser:')) return allowFamily(['browser.']);
|
|
66
|
+
if (resourceKey === 'desktop:windows') return allowFamily(['app.', 'visual.', 'input.']);
|
|
67
|
+
if (resourceKey.startsWith('process:')) return allowFamily(['process.', 'terminal.']);
|
|
68
|
+
if (resourceKey.startsWith('cap:')) {
|
|
69
|
+
const capability = resourceKey.slice('cap:'.length);
|
|
70
|
+
return CAPABILITY.test(capability)
|
|
71
|
+
&& required.includes(capability)
|
|
72
|
+
&& profileAllows(capability, permissions.allowedCapabilities);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function hashAuthorizedMigrationArtifact(
|
|
78
|
+
filePathInput ,
|
|
79
|
+
permissions
|
|
80
|
+
) {
|
|
81
|
+
if (permissions.allowedRoots.length < 1) throw new OperatorError('SEMANTIC_MIGRATION_ARTIFACT_DENIED', 'No filesystem roots are authorized.');
|
|
82
|
+
const scope = new PathScope(permissions.allowedRoots);
|
|
83
|
+
return await scope.withExisting(bounded(filePathInput, 4096, 'artifact path'), async (resolved) => {
|
|
84
|
+
const stat = await fs.lstat(resolved);
|
|
85
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
86
|
+
throw new OperatorError('SEMANTIC_MIGRATION_ARTIFACT_DENIED', 'Migration artifacts must be regular files.');
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isSafeInteger(stat.size) || stat.size < 0) throw new OperatorError('SEMANTIC_MIGRATION_ARTIFACT_DENIED', 'Migration artifact size is invalid.');
|
|
89
|
+
const hash = crypto.createHash('sha256');
|
|
90
|
+
for await (const chunk of createReadStream(resolved, { highWaterMark: 1024 * 1024 })) hash.update(chunk );
|
|
91
|
+
return { digest: hash.digest('hex'), size: stat.size };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function buildMigrationArtifacts(
|
|
96
|
+
artifactsInput ,
|
|
97
|
+
permissions
|
|
98
|
+
) {
|
|
99
|
+
if (!Array.isArray(artifactsInput) || artifactsInput.length > 5000) {
|
|
100
|
+
throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'artifacts must contain at most 5000 entries.');
|
|
101
|
+
}
|
|
102
|
+
const seen = new Set ();
|
|
103
|
+
const artifacts = [];
|
|
104
|
+
for (const [index, item] of artifactsInput.entries()) {
|
|
105
|
+
const key = bounded(item?.key, 1024, `artifacts[${index}].key`);
|
|
106
|
+
if (seen.has(key)) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'Artifact keys must be unique.');
|
|
107
|
+
seen.add(key);
|
|
108
|
+
const proof = await hashAuthorizedMigrationArtifact(item?.path, permissions);
|
|
109
|
+
artifacts.push({ key, digest: proof.digest, size: proof.size });
|
|
110
|
+
}
|
|
111
|
+
return artifacts.sort((a, b) => a.key.localeCompare(b.key));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function buildMigrationWorldAssumptions(
|
|
115
|
+
assumptionsInput ,
|
|
116
|
+
world
|
|
117
|
+
) {
|
|
118
|
+
if (!Array.isArray(assumptionsInput) || assumptionsInput.length > 5000) {
|
|
119
|
+
throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'worldAssumptions must contain at most 5000 entries.');
|
|
120
|
+
}
|
|
121
|
+
const assumptions = [];
|
|
122
|
+
const seen = new Set ();
|
|
123
|
+
for (const [index, item] of assumptionsInput.entries()) {
|
|
124
|
+
const entityKey = bounded(item?.entityKey, 512, `worldAssumptions[${index}].entityKey`);
|
|
125
|
+
const factKey = bounded(item?.factKey, 128, `worldAssumptions[${index}].factKey`);
|
|
126
|
+
const identity = `${entityKey}\0${factKey}`;
|
|
127
|
+
if (seen.has(identity)) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'World assumptions must be unique.');
|
|
128
|
+
seen.add(identity);
|
|
129
|
+
const fact = await world.resolveFact(entityKey, factKey);
|
|
130
|
+
if (fact.status !== 'resolved') {
|
|
131
|
+
throw new OperatorError('SEMANTIC_MIGRATION_WORLD_UNRESOLVED', `World assumption ${entityKey}.${factKey} is ${fact.status}.`);
|
|
132
|
+
}
|
|
133
|
+
assumptions.push({ entityKey, factKey, valueDigest: worldValueDigest(fact.value) });
|
|
134
|
+
}
|
|
135
|
+
return assumptions.sort((a, b) => `${a.entityKey}\0${a.factKey}`.localeCompare(`${b.entityKey}\0${b.factKey}`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function verifyMigrationWorldAssumption(
|
|
139
|
+
assumption ,
|
|
140
|
+
world
|
|
141
|
+
) {
|
|
142
|
+
const fact = await world.resolveFact(assumption.entityKey, assumption.factKey);
|
|
143
|
+
return fact.status === 'resolved' ? worldValueDigest(fact.value) : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function physicalResource(resourceKey ) {
|
|
147
|
+
for (const [prefix, families] of [
|
|
148
|
+
['file:', ['file.']],
|
|
149
|
+
['repo:', ['git.', 'project.']],
|
|
150
|
+
['workspace:', ['terminal.', 'project.']],
|
|
151
|
+
['docker:', ['docker.']]
|
|
152
|
+
] ) {
|
|
153
|
+
if (resourceKey.startsWith(prefix)) {
|
|
154
|
+
const target = resourceKey.slice(prefix.length);
|
|
155
|
+
return target ? { path: target, capabilityPrefixes: [...families] } : undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (resourceKey.startsWith('database:')) {
|
|
159
|
+
const value = resourceKey.slice('database:'.length);
|
|
160
|
+
const split = value.lastIndexOf(':');
|
|
161
|
+
const root = split > 0 ? value.slice(0, split) : value;
|
|
162
|
+
return root ? { path: root, capabilityPrefixes: ['postgres.'] } : undefined;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function profileAllows(capability , allowed ) {
|
|
168
|
+
return allowed.some((rule) => rule === capability || (rule.endsWith('.*') && capability.startsWith(rule.slice(0, -1))));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function exactCapabilities(input ) {
|
|
172
|
+
if (!Array.isArray(input) || input.length > 2048) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'requiredCapabilities is invalid.');
|
|
173
|
+
const values = input.map((item, index) => {
|
|
174
|
+
const value = bounded(item, 256, `requiredCapabilities[${index}]`);
|
|
175
|
+
if (!CAPABILITY.test(value) || value.includes('*')) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'Migration capabilities must be exact capability names.');
|
|
176
|
+
return value;
|
|
177
|
+
});
|
|
178
|
+
if (new Set(values).size !== values.length) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'Migration capabilities contain duplicates.');
|
|
179
|
+
return values.sort();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizedResourceKeys(input ) {
|
|
183
|
+
if (!Array.isArray(input) || input.length > 5000) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'resourceKeys is invalid.');
|
|
184
|
+
const values = input.map((item, index) => bounded(item, 1024, `resourceKeys[${index}]`));
|
|
185
|
+
if (new Set(values).size !== values.length) throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', 'resourceKeys contain duplicates.');
|
|
186
|
+
return values.sort();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bounded(input , max , label ) {
|
|
190
|
+
if (typeof input !== 'string' || input.length < 1 || input.length > max || input.includes('\0')) {
|
|
191
|
+
throw new OperatorError('SEMANTIC_MIGRATION_INPUT_INVALID', `${label} is invalid.`);
|
|
192
|
+
}
|
|
193
|
+
return input;
|
|
194
|
+
}
|