@mogragsigma/dsh-dual-host-plugin 0.0.0-stage → 0.33.2
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/LICENSE +58 -0
- package/LICENSE.en.md +94 -0
- package/README.md +88 -2
- package/bundle/ASSET_NOTICE.md +19 -0
- package/bundle/LICENSE +58 -0
- package/bundle/LICENSE.en.md +94 -0
- package/bundle/README.md +75 -0
- package/bundle/THIRD_PARTY_NOTICES.md +94 -0
- package/bundle/a2a-dispatches.js +38 -0
- package/bundle/a2a-endpoint.js +112 -0
- package/bundle/a2a-tools.js +11 -0
- package/bundle/a2a-worker.js +63 -0
- package/bundle/approval-auth.js +5 -0
- package/bundle/approval-chat.js +150 -0
- package/bundle/approval-feishu.js +52 -0
- package/bundle/approval-guidance.js +68 -0
- package/bundle/approval-http.js +8 -0
- package/bundle/approval-ledger.js +142 -0
- package/bundle/approval-local-relay.js +86 -0
- package/bundle/approval-receipts.js +62 -0
- package/bundle/approval-reference.js +33 -0
- package/bundle/approval-semantic-model.js +26 -0
- package/bundle/approval-semantics.js +51 -0
- package/bundle/approval-ui/app.js +1 -0
- package/bundle/approval-ui/index.html +1 -0
- package/bundle/approval-ui/style.css +1 -0
- package/bundle/approval-workbench.js +45 -0
- package/bundle/artifact-media.js +34 -0
- package/bundle/bounded-text.js +44 -0
- package/bundle/compatibility.json +980 -0
- package/bundle/cordis.patch.yml +5 -0
- package/bundle/delivery-approval.js +133 -0
- package/bundle/delivery-artifacts.js +52 -0
- package/bundle/delivery-commit.js +125 -0
- package/bundle/delivery-contract.js +42 -0
- package/bundle/delivery-git-read.js +217 -0
- package/bundle/delivery-index.js +23 -0
- package/bundle/delivery-journal.js +207 -0
- package/bundle/delivery-object-plan.js +138 -0
- package/bundle/delivery-preview.js +153 -0
- package/bundle/delivery-process-receipts.js +90 -0
- package/bundle/delivery-process.js +46 -0
- package/bundle/delivery-process.py +228 -0
- package/bundle/delivery-push-approval.js +89 -0
- package/bundle/delivery-push-plan.js +98 -0
- package/bundle/delivery-push-recovery.js +32 -0
- package/bundle/delivery-push.js +83 -0
- package/bundle/delivery-recovery.js +143 -0
- package/bundle/delivery-runtime.js +182 -0
- package/bundle/delivery-ssh.js +64 -0
- package/bundle/delivery-state.js +106 -0
- package/bundle/development-attention.js +35 -0
- package/bundle/development-bridge.js +136 -0
- package/bundle/development-command-review.js +110 -0
- package/bundle/development-container.js +110 -0
- package/bundle/development-contract.js +17 -0
- package/bundle/development-files.js +101 -0
- package/bundle/development-gate.js +236 -0
- package/bundle/development-model-budget.js +48 -0
- package/bundle/development-peer.js +155 -0
- package/bundle/development-plan.js +43 -0
- package/bundle/development-reconciliation.js +62 -0
- package/bundle/development-review.js +81 -0
- package/bundle/development-rules/AGENTS.md +51 -0
- package/bundle/development-runtime.js +68 -0
- package/bundle/development-source.js +48 -0
- package/bundle/development-test-runner.js +13 -0
- package/bundle/development-tools.js +45 -0
- package/bundle/development-worker-review.js +110 -0
- package/bundle/development-worker.js +68 -0
- package/bundle/development-workspace.js +54 -0
- package/bundle/diagnostics.js +78 -0
- package/bundle/distribution-lock.json +773 -0
- package/bundle/drained-process.js +26 -0
- package/bundle/durable-inbox.js +294 -0
- package/bundle/feishu-policy.js +24 -0
- package/bundle/gateway-maintenance.js +55 -0
- package/bundle/global-rules/AGENTS.md +83 -0
- package/bundle/im-delivery.js +60 -0
- package/bundle/index.js +445 -0
- package/bundle/intervention-actions.js +135 -0
- package/bundle/intervention-job-control.js +28 -0
- package/bundle/intervention-state.js +43 -0
- package/bundle/job-version.js +15 -0
- package/bundle/legacy-feishu-adapter.js +89 -0
- package/bundle/link-contract.js +80 -0
- package/bundle/maintenance-process-identity.js +93 -0
- package/bundle/model-selection.js +71 -0
- package/bundle/native-context.js +14 -0
- package/bundle/native-entry-admission.js +2 -0
- package/bundle/native-entry.js +58 -0
- package/bundle/native-file-reference.js +32 -0
- package/bundle/native-owner-origin.js +12 -0
- package/bundle/native-rules/AGENTS.md +199 -0
- package/bundle/native-tools.js +108 -0
- package/bundle/notification-artifacts.js +70 -0
- package/bundle/notification-contract.js +29 -0
- package/bundle/notification-defaults.js +81 -0
- package/bundle/notification-discovery.js +65 -0
- package/bundle/notification-endpoint.js +54 -0
- package/bundle/notification-feishu.js +51 -0
- package/bundle/notification-media-endpoint.js +94 -0
- package/bundle/notification-review.js +27 -0
- package/bundle/notification-targets.js +54 -0
- package/bundle/onboarding-activity.js +66 -0
- package/bundle/onboarding-agent-hold.js +94 -0
- package/bundle/onboarding-boundary.js +20 -0
- package/bundle/onboarding-cloud-contract.js +29 -0
- package/bundle/onboarding-cloud-file-review.js +58 -0
- package/bundle/onboarding-cloud-files.js +100 -0
- package/bundle/onboarding-cloud-im.js +34 -0
- package/bundle/onboarding-cloud-model-probe.js +53 -0
- package/bundle/onboarding-cloud-models.js +68 -0
- package/bundle/onboarding-cloud-operation.js +276 -0
- package/bundle/onboarding-cloud-package.js +38 -0
- package/bundle/onboarding-cloud-repositories.js +89 -0
- package/bundle/onboarding-cloud-retirement.js +59 -0
- package/bundle/onboarding-cloud-task-runtime.js +90 -0
- package/bundle/onboarding-cloud-web.js +85 -0
- package/bundle/onboarding-cloud.js +110 -0
- package/bundle/onboarding-coordinated-hold.js +94 -0
- package/bundle/onboarding-exclusive-rename.js +16 -0
- package/bundle/onboarding-existing-host.js +63 -0
- package/bundle/onboarding-existing-setup.js +123 -0
- package/bundle/onboarding-file-contract.js +31 -0
- package/bundle/onboarding-file-review-contract.js +38 -0
- package/bundle/onboarding-file-transfer.js +133 -0
- package/bundle/onboarding-handoff-remote.js +98 -0
- package/bundle/onboarding-handoff.js +112 -0
- package/bundle/onboarding-host-stop-observation.js +44 -0
- package/bundle/onboarding-host.js +254 -0
- package/bundle/onboarding-identity.js +9 -0
- package/bundle/onboarding-im-maintenance-native.js +30 -0
- package/bundle/onboarding-im-maintenance.js +70 -0
- package/bundle/onboarding-im.js +31 -0
- package/bundle/onboarding-inspection-guard.js +15 -0
- package/bundle/onboarding-install-fs.js +58 -0
- package/bundle/onboarding-installer.js +157 -0
- package/bundle/onboarding-local-pair-native.js +21 -0
- package/bundle/onboarding-local-pair.js +99 -0
- package/bundle/onboarding-maintenance-fs.js +69 -0
- package/bundle/onboarding-maintenance-plan.js +213 -0
- package/bundle/onboarding-maintenance-record.js +169 -0
- package/bundle/onboarding-maintenance.js +163 -0
- package/bundle/onboarding-model-probe.js +119 -0
- package/bundle/onboarding-models.js +107 -0
- package/bundle/onboarding-native-host.js +101 -0
- package/bundle/onboarding-native-probe.js +56 -0
- package/bundle/onboarding-native-runner.js +29 -0
- package/bundle/onboarding-node-bootstrap.py +678 -0
- package/bundle/onboarding-node.js +77 -0
- package/bundle/onboarding-notification-defaults.js +93 -0
- package/bundle/onboarding-package-source.js +33 -0
- package/bundle/onboarding-paths.js +58 -0
- package/bundle/onboarding-peer-binding.js +61 -0
- package/bundle/onboarding-peer-contract.js +12 -0
- package/bundle/onboarding-plugin-repair.js +116 -0
- package/bundle/onboarding-plugin-version.js +143 -0
- package/bundle/onboarding-remote-control.js +140 -0
- package/bundle/onboarding-remote-probe.js +40 -0
- package/bundle/onboarding-repositories.js +88 -0
- package/bundle/onboarding-repository.js +49 -0
- package/bundle/onboarding-runtime-activation.js +83 -0
- package/bundle/onboarding-runtime-artifacts-client.js +87 -0
- package/bundle/onboarding-runtime-artifacts.js +126 -0
- package/bundle/onboarding-runtime-extension.js +400 -0
- package/bundle/onboarding-runtime-inventory.js +88 -0
- package/bundle/onboarding-runtime-program.js +42 -0
- package/bundle/onboarding-runtime-reclaim-client.js +94 -0
- package/bundle/onboarding-runtime-reclaim-worker.js +63 -0
- package/bundle/onboarding-runtime-reclaim.js +132 -0
- package/bundle/onboarding-runtime-repair-client.js +50 -0
- package/bundle/onboarding-runtime-repair.js +88 -0
- package/bundle/onboarding-runtime-stop.js +90 -0
- package/bundle/onboarding-runtime-uninstall-client.js +88 -0
- package/bundle/onboarding-runtime-uninstall.js +167 -0
- package/bundle/onboarding-runtime-version-client.js +138 -0
- package/bundle/onboarding-runtime-version.js +145 -0
- package/bundle/onboarding-schema.js +42 -0
- package/bundle/onboarding-self-disable.js +3 -0
- package/bundle/onboarding-self-lifecycle.js +95 -0
- package/bundle/onboarding-self-maintenance.js +52 -0
- package/bundle/onboarding-self-remove.js +3 -0
- package/bundle/onboarding-self-stop.js +85 -0
- package/bundle/onboarding-ssh-forward.js +48 -0
- package/bundle/onboarding-ssh.js +105 -0
- package/bundle/onboarding-state.js +95 -0
- package/bundle/onboarding-task-runtime-contract.js +38 -0
- package/bundle/onboarding-task-runtime-native.js +80 -0
- package/bundle/onboarding-task-runtime.js +66 -0
- package/bundle/onboarding-token-plan.js +52 -0
- package/bundle/onboarding-version-activation-client.js +48 -0
- package/bundle/onboarding-version-candidate.js +96 -0
- package/bundle/onboarding-version-repair.js +128 -0
- package/bundle/package-lock.json +773 -0
- package/bundle/package.json +99 -0
- package/bundle/peer-file-fs.js +62 -0
- package/bundle/peer-file-migration.js +59 -0
- package/bundle/peer-file-review.js +107 -0
- package/bundle/peer-files.js +157 -0
- package/bundle/peer-mailbox.js +75 -0
- package/bundle/protocol.js +17 -0
- package/bundle/recovery.js +40 -0
- package/bundle/repository-catalog.js +85 -0
- package/bundle/repository-gitee.js +76 -0
- package/bundle/repository-managed.js +46 -0
- package/bundle/repository-writer.js +43 -0
- package/bundle/rules/AGENTS.md +22 -0
- package/bundle/runtime-im-lock.json +11577 -0
- package/bundle/runtime-im-package.json +18 -0
- package/bundle/runtime-lock.json +10152 -0
- package/bundle/runtime-package.json +17 -0
- package/bundle/scheduler-migration.js +45 -0
- package/bundle/search-contract.js +19 -0
- package/bundle/skills/dsh-dual-end/SKILL.md +12 -0
- package/bundle/skills/task-a2a/SKILL.md +55 -0
- package/bundle/skills/task-a2a-worker/SKILL.md +23 -0
- package/bundle/skills/task-approval/SKILL.md +128 -0
- package/bundle/skills/task-artifacts/SKILL.md +46 -0
- package/bundle/skills/task-development/SKILL.md +144 -0
- package/bundle/skills/task-diagnostics/SKILL.md +41 -0
- package/bundle/skills/task-direct-link/SKILL.md +143 -0
- package/bundle/skills/task-file-transfer/SKILL.md +107 -0
- package/bundle/skills/task-handoff/SKILL.md +23 -0
- package/bundle/skills/task-intake/SKILL.md +13 -0
- package/bundle/skills/task-notify/SKILL.md +102 -0
- package/bundle/skills/task-onboarding/SKILL.md +708 -0
- package/bundle/skills/task-recovery/SKILL.md +54 -0
- package/bundle/skills/task-repository/SKILL.md +145 -0
- package/bundle/skills/task-research/SKILL.md +43 -0
- package/bundle/skills/task-schedule/SKILL.md +79 -0
- package/bundle/skills/task-sidebar/SKILL.md +122 -0
- package/bundle/skills/task-workbench/SKILL.md +30 -0
- package/bundle/skills/task-worker/SKILL.md +88 -0
- package/bundle/source-search.js +89 -0
- package/bundle/source-web.js +17 -0
- package/bundle/ssh-connector.js +24 -0
- package/bundle/store.js +102 -0
- package/bundle/task-a2a.js +298 -0
- package/bundle/task-artifacts.js +179 -0
- package/bundle/task-board-a2a.js +55 -0
- package/bundle/task-board-development.js +83 -0
- package/bundle/task-board-planning.js +84 -0
- package/bundle/task-board.js +132 -0
- package/bundle/task-branch.js +2 -0
- package/bundle/task-catalog.js +77 -0
- package/bundle/task-context.js +40 -0
- package/bundle/task-envelope.js +96 -0
- package/bundle/task-interventions.js +54 -0
- package/bundle/task-jobs.js +156 -0
- package/bundle/task-notifications.js +132 -0
- package/bundle/task-results.js +16 -0
- package/bundle/task-scheduler.js +77 -0
- package/bundle/task-sources.js +101 -0
- package/bundle/task-trigger-time.js +46 -0
- package/bundle/task-triggers.js +157 -0
- package/bundle/task-worker.js +73 -0
- package/bundle/task-workflows.js +117 -0
- package/bundle/tools.js +13 -0
- package/bundle/workbench-assets/asset-provenance.json +26 -0
- package/bundle/workbench-assets/whale-ui-states.png +0 -0
- package/bundle/workbench-client.js +811 -0
- package/bundle/workbench-decisions.js +48 -0
- package/bundle/workbench-host.js +122 -0
- package/bundle/workbench-im-identity.js +51 -0
- package/bundle/workbench-proxy.js +127 -0
- package/bundle/worker-rules/AGENTS.md +84 -0
- package/bundle/worker-tools.js +13 -0
- package/compatibility.json +980 -0
- package/cordis.patch.yml +3 -0
- package/package.json +57 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Operator-only persistent reclamation requests; never retry an uncertain send.
|
|
2
|
+
import {createHash,randomUUID} from 'node:crypto';
|
|
3
|
+
import {credentialKey} from '@deepseek-ai/dsh-credentials';
|
|
4
|
+
import {runtimeProgram,runtimeCommand} from './onboarding-runtime-program.js';
|
|
5
|
+
const hash=x=>createHash('sha256').update(JSON.stringify(x)).digest('hex');
|
|
6
|
+
const fail=c=>{throw Error('ONBOARDING_CLOUD_RECLAIM_'+c);};
|
|
7
|
+
const exact=(v,k)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join()===k.slice().sort().join();
|
|
8
|
+
const uuid=x=>typeof x==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(x);
|
|
9
|
+
const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x),integer=x=>Number.isSafeInteger(x)&&x>=0;
|
|
10
|
+
const scope='stopped_owned_runtime_keep_user_data';
|
|
11
|
+
export const reclaimRecordKey=id=>credentialKey('dsh-task-gateway','onboarding-runtime-reclaim-v1-'+id);
|
|
12
|
+
const active=d=>d?.requests?.filter(r=>r.receipt?.state!=='refused')??[];
|
|
13
|
+
export const reclaimBlocked=d=>active(d).length>0;
|
|
14
|
+
export const reclaimPending=d=>active(d).some(r=>r.state!=='observed'||r.receipt?.state!=='committed');
|
|
15
|
+
const common=(operationId,runtimeId)=>({contract:1,operationId,runtimeId,scope,hostStarted:false,initialization:false,automaticRetry:false,userDataPreserved:true,backupsRetained:true,userDataDeleted:false,irreversible:true});
|
|
16
|
+
const fields=['contract','operationId','runtimeId','scope','hostStarted','initialization','automaticRetry','userDataPreserved','backupsRetained','userDataDeleted','irreversible','id','state','completed','total','recoveryRequired','managedRestartBlocked','runtimeFilesRemoved'];
|
|
17
|
+
const receiptFields=v=>[...fields,...(Object.hasOwn(v??{},'backgroundSupported')?['backgroundSupported','workerActive','executionState']:[])];
|
|
18
|
+
function check(v,d,id,{absent=false,allowRefused=false}={}){
|
|
19
|
+
if(!v||Object.entries(common(d.operationId,d.runtimeId)).some(([k,x])=>v[k]!==x))fail('RESPONSE_REVIEW');
|
|
20
|
+
const background=Object.hasOwn(v,'backgroundSupported');
|
|
21
|
+
if(background&&(v.backgroundSupported!==true||typeof v.workerActive!=='boolean'||v.executionState!==(v.state==='not_requested'?'not_requested':v.state==='committed'?'completed':v.state==='deleting'?(v.workerActive?'running':'interrupted'):null)||v.state==='not_requested'&&v.workerActive))fail('RESPONSE_REVIEW');
|
|
22
|
+
if(allowRefused&&v.state==='refused'){if(v.id!==id||v.completed!==0||v.total!==0||v.recoveryRequired!==false||v.managedRestartBlocked!==false||v.runtimeFilesRemoved!==false)fail('RESPONSE_REVIEW');return;}
|
|
23
|
+
if(absent&&v.state==='not_requested'){
|
|
24
|
+
if(v.id!==null||v.completed!==0||v.total!==0||v.recoveryRequired!==false||v.managedRestartBlocked!==false||v.runtimeFilesRemoved!==false)fail('RESPONSE_REVIEW');return;
|
|
25
|
+
}
|
|
26
|
+
if(v.id!==id||!['deleting','committed'].includes(v.state)||!integer(v.completed)||!integer(v.total)||v.total<1||v.total>150000||v.completed>v.total||v.state==='committed'&&v.completed!==v.total||v.recoveryRequired!==(v.state==='deleting'&&!(background&&v.workerActive))||v.managedRestartBlocked!==true||v.runtimeFilesRemoved!==(v.state==='committed'))fail('RESPONSE_REVIEW');
|
|
27
|
+
}
|
|
28
|
+
export function parseReclaimRequest(record,operationId){
|
|
29
|
+
if(!record)return null;const d=record.payload;
|
|
30
|
+
if(record.kind!=='grant'||!exact(d,['schema','operationId','runtimeId','binding','revision','plans','requests'])||d.schema!==1||d.operationId!==operationId||!uuid(d.runtimeId)||!hex(d.binding)||!integer(d.revision)||d.revision<1||!Array.isArray(d.plans)||d.plans.length>128||!Array.isArray(d.requests)||d.requests.length>128||Buffer.byteLength(JSON.stringify(d))>262144)fail('LOCAL_LEDGER_REVIEW');
|
|
31
|
+
const ids=new Set();for(const p of d.plans){if(!exact(p,['id','kind','targetId','fingerprint','expiresAt'])||!uuid(p.id)||ids.has(p.id)||!['apply','recover'].includes(p.kind)||!uuid(p.targetId)||!hex(p.fingerprint)||!integer(p.expiresAt))fail('LOCAL_LEDGER_REVIEW');ids.add(p.id);}
|
|
32
|
+
for(const p of d.plans)if(p.kind==='apply'?p.id!==p.targetId:!d.plans.some(x=>x.kind==='apply'&&x.id===p.targetId))fail('LOCAL_LEDGER_REVIEW');
|
|
33
|
+
const keys=new Set();for(const r of d.requests){const p=d.plans.find(x=>x.id===r?.planId&&x.kind===r?.action);
|
|
34
|
+
if(!exact(r,['key','action','planId','targetId','fingerprint','state','receipt','observedAbsent'])||!p||r.key!==r.action+':'+r.planId||keys.has(r.key)||r.targetId!==p.targetId||r.fingerprint!==p.fingerprint||!['requested','observed'].includes(r.state)||typeof r.observedAbsent!=='boolean'||r.state==='requested'&&r.receipt!==null||r.state==='observed'&&r.observedAbsent)fail('LOCAL_LEDGER_REVIEW');keys.add(r.key);
|
|
35
|
+
if(r.state==='observed'){if(!exact(r.receipt,receiptFields(r.receipt)))fail('LOCAL_LEDGER_REVIEW');try{check(r.receipt,d,r.targetId,{allowRefused:true});}catch{fail('LOCAL_LEDGER_REVIEW');}}
|
|
36
|
+
}if(new Set(active(d).map(r=>r.targetId)).size>1)fail('LOCAL_LEDGER_REVIEW');return d;
|
|
37
|
+
}
|
|
38
|
+
export async function runtimeReclaimClient({args,p,a,b,runtimeId,credentials,ssh,now=Date.now}){
|
|
39
|
+
const actions={reclaim_record:[],reclaim_preview:[],reclaim_apply:['id','fingerprint','confirm'],reclaim_status:['id'],reclaim_recover_preview:['id'],reclaim_recover:['id','fingerprint','confirm']},action=args?.action,kind=action?.slice(8),mutation=['reclaim_apply','reclaim_recover'].includes(action);
|
|
40
|
+
if(!Object.hasOwn(actions,action)||!exact(args,['action','operationId',...actions[action]])||args.operationId!==p.id||Object.hasOwn(args,'id')&&!uuid(args.id)||mutation&&(args.confirm!==true||!hex(args.fingerprint)))fail('CONFIRMATION_REQUIRED');
|
|
41
|
+
const c=credentials(),key=reclaimRecordKey(p.id),binding=hash({operationId:p.id,archiveHash:p.archiveHash,bindingFingerprint:p.bindingFingerprint});let ledger=parseReclaimRequest(await c.readRecord(key),p.id);
|
|
42
|
+
if(ledger&&(ledger.binding!==binding||runtimeId&&ledger.runtimeId!==runtimeId))fail('BINDING_CHANGED');
|
|
43
|
+
const decode=n=>{const f=a.files.find(x=>x.path==='plugin/'+n);if(!f)fail('ARCHIVE_REVIEW');return Buffer.from(f.data,'base64').toString();},compat=JSON.parse(decode('compatibility.json'));
|
|
44
|
+
const workerSupported=compat.onboardingRuntimeReclamationWorker===1;
|
|
45
|
+
const supported=['onboardingPluginVersion','onboardingPluginVersionLifecycle','onboardingPluginVersionArtifacts','onboardingPluginUninstall','onboardingRuntimeReclamation','onboardingRuntimeReclamationRequests'].every(k=>compat[k]===1);
|
|
46
|
+
if(action==='reclaim_record')return {operationId:p.id,runtimeId:ledger?.runtimeId??runtimeId??null,supported,workerSupported,pending:reclaimPending(ledger),blocked:reclaimBlocked(ledger),plans:ledger?.plans??[],requests:ledger?.requests??[],cached:true,historical:true,hostStarted:false,initialization:false};
|
|
47
|
+
if(!supported)fail('UNSUPPORTED');if(!uuid(runtimeId))fail('RUNTIME_RECORD_REQUIRED');
|
|
48
|
+
const empty={schema:1,operationId:p.id,runtimeId,binding,revision:0,plans:[],requests:[]},current=()=>ledger??empty;
|
|
49
|
+
async function save(next){const old=ledger,value={...next,revision:(ledger?.revision??0)+1};parseReclaimRequest({kind:'grant',payload:value},p.id);await c.modifyRecord(key,r=>{if(JSON.stringify(parseReclaimRequest(r,p.id))!==JSON.stringify(old))fail('LOCAL_CONFLICT');return {kind:'grant',payload:value};});ledger=value;}
|
|
50
|
+
const prior=mutation?current().requests.find(r=>r.key===kind+':'+args.id):null;
|
|
51
|
+
if(prior){if(prior.fingerprint!==args.fingerprint)fail('PLAN_DRIFT');return {...prior.receipt??common(p.id,runtimeId),id:prior.targetId,state:prior.receipt?.state??'reclamation_outcome_unknown',cached:true,historical:true,notResent:true,requestState:prior.state};}
|
|
52
|
+
if(['reclaim_preview','reclaim_apply'].includes(action)&&reclaimBlocked(ledger))fail('RECOVERY_REQUIRED');
|
|
53
|
+
let plan,target=args.id,remoteArgs={};
|
|
54
|
+
if(mutation){plan=current().plans.find(x=>x.id===args.id&&x.kind===kind);if(!plan||plan.fingerprint!==args.fingerprint||plan.expiresAt<=now())fail('PLAN_EXPIRED');target=plan.targetId;remoteArgs={id:target,fingerprint:plan.fingerprint,confirm:true};}
|
|
55
|
+
else if(action!=='reclaim_preview'&&!current().plans.some(x=>x.kind==='apply'&&x.id===args.id))fail('TRANSACTION_UNKNOWN');
|
|
56
|
+
const last=id=>current().requests.filter(r=>r.targetId===id).at(-1);
|
|
57
|
+
if(kind==='recover'||kind==='recover_preview'){
|
|
58
|
+
const r=last(target);if(!r||r.state==='requested'&&!r.observedAbsent||r.receipt?.state==='committed'||r.receipt?.workerActive===true)fail('OUTCOME_UNKNOWN');
|
|
59
|
+
}
|
|
60
|
+
const input=JSON.stringify({action:'runtime_version',root:p.root,operationId:p.id,installationId:p.installationId,proof:p.proof,planHash:p.planHash,versionAction:action==='reclaim_recover_preview'?'reclaim_recoveryPreview':action,args:remoteArgs});
|
|
61
|
+
const command=runtimeCommand(a.baseline.node.path,runtimeProgram(decode('onboarding-remote-control.js'),decode('onboarding-runtime-extension.js')),'runtimeVersionCLI');
|
|
62
|
+
if(mutation){if(current().requests.length>=128)fail('HISTORY_FULL');await save({...current(),requests:[...current().requests,{key:kind+':'+args.id,action:kind,planId:args.id,targetId:target,fingerprint:plan.fingerprint,state:'requested',receipt:null,observedAbsent:false}]});}
|
|
63
|
+
let result;try{result=JSON.parse(await ssh.execute(b,command,input,{timeoutMs:120000}));}catch{fail('OUTCOME_UNKNOWN');}
|
|
64
|
+
if(result?.ok!==true){
|
|
65
|
+
// This exact target error is emitted only before creating any deletion intent.
|
|
66
|
+
// Other errors can happen after partial deletion and must remain unknown.
|
|
67
|
+
if(action==='reclaim_apply'&&result?.error==='ONBOARDING_CLOUD_RUNTIME_RECLAIM_PLAN_EXPIRED'){
|
|
68
|
+
const receipt={...common(p.id,runtimeId),id:target,state:'refused',completed:0,total:0,recoveryRequired:false,managedRestartBlocked:false,runtimeFilesRemoved:false};
|
|
69
|
+
await save({...current(),requests:current().requests.map(r=>r.key===kind+':'+args.id?{...r,state:'observed',receipt,observedAbsent:false}:r)});
|
|
70
|
+
}
|
|
71
|
+
throw Error(typeof result?.error==='string'&&/^ONBOARDING_CLOUD_[A-Z_]{1,120}$/.test(result.error)?result.error:'ONBOARDING_CLOUD_RECLAIM_RESPONSE_REVIEW');
|
|
72
|
+
}
|
|
73
|
+
const v=result.value,base=common(p.id,runtimeId);if(!v||Object.entries(base).some(([k,x])=>v[k]!==x))fail('RESPONSE_REVIEW');
|
|
74
|
+
if(['reclaim_preview','reclaim_recover_preview'].includes(action)){
|
|
75
|
+
const recovery=kind==='recover_preview';if(!uuid(v.id)||!hex(v.fingerprint)||!recovery&&(!integer(v.expiresAt)||v.expiresAt<=now()||v.expiresAt>now()+301000||v.canApply!==true||!integer(v.entries)||v.entries<1||v.entries>150000||!integer(v.files)||v.files>v.entries||!integer(v.bytes)||!Array.isArray(v.removeRoots)||v.removeRoots.length>256||!Array.isArray(v.retainedPaths)||v.retainedPaths.length>16)||v.requiresExplicitConfirmation!==true)fail('RESPONSE_REVIEW');
|
|
76
|
+
if(recovery){
|
|
77
|
+
if(v.id!==target||!['continue_original_manifest_only','start_original_manifest_if_still_unobserved'].includes(v.action)||v.recoveryRequired!==true)fail('RESPONSE_REVIEW');
|
|
78
|
+
if(v.action==='continue_original_manifest_only')check(v,current(),target);
|
|
79
|
+
else if(v.state!=='unobserved'||v.completed!==0||!integer(v.total)||v.total<1||v.total>150000||v.managedRestartBlocked!==true||v.runtimeFilesRemoved!==false)fail('RESPONSE_REVIEW');
|
|
80
|
+
}
|
|
81
|
+
if(!recovery&&(!v.removeRoots.length||v.removeRoots.some(x=>typeof x!=='string'||!/^runtime\/[^/]+$/.test(x)||['runtime/plugin','runtime/native-home','runtime/native-tmp','runtime/workspace','runtime/.','runtime/..'].includes(x))||hash(v.retainedPaths.slice().sort())!==hash(['runtime/native-home','runtime/native-tmp','runtime/workspace','runtime','.control','plugin'].sort())))fail('RESPONSE_REVIEW');
|
|
82
|
+
if(!recovery&&[...v.removeRoots,...v.retainedPaths].some(x=>typeof x!=='string'||x.length>300||/[\x00-\x1f\x7f]/.test(x)))fail('RESPONSE_REVIEW');
|
|
83
|
+
if(current().plans.length>=128)fail('HISTORY_FULL');const id=recovery?randomUUID():v.id;if(current().plans.some(x=>x.id===id))fail('PLAN_DRIFT');const expiresAt=recovery?now()+300000:v.expiresAt;
|
|
84
|
+
await save({...current(),plans:[...current().plans,{id,kind:recovery?'recover':'apply',targetId:v.id,fingerprint:v.fingerprint,expiresAt}]});return {...v,id,transactionId:v.id,expiresAt,cached:false};
|
|
85
|
+
}
|
|
86
|
+
if(workerSupported&&v.backgroundSupported!==true)fail('RESPONSE_REVIEW');
|
|
87
|
+
check(v,current(),target,{absent:action==='reclaim_status'});
|
|
88
|
+
if(v.state==='not_requested'){
|
|
89
|
+
if(last(target))await save({...current(),requests:current().requests.map(r=>r.targetId===target&&r.state==='requested'?{...r,observedAbsent:true}:r)});
|
|
90
|
+
return {...v,id:target,state:'reclamation_outcome_unknown',observedState:'not_requested',outcomeUnknown:true,historical:true,cached:false};
|
|
91
|
+
}
|
|
92
|
+
const receipt=Object.fromEntries(receiptFields(v).map(k=>[k,v[k]]));if(last(target))await save({...current(),requests:current().requests.map(r=>r.targetId===target?{...r,state:'observed',receipt,observedAbsent:false}:r)});
|
|
93
|
+
return {...receipt,cached:false,historical:action==='reclaim_status'};
|
|
94
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Original-archive worker authority. A launch is separate from deletion success.
|
|
2
|
+
// No retries: a dead/unknown worker requires a new explicit recovery approval.
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {spawn as runtimeSpawn} from 'node:child_process';
|
|
5
|
+
const uuid=x=>typeof x==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(x);
|
|
6
|
+
const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x);
|
|
7
|
+
const exact=(x,k)=>x&&typeof x==='object'&&!Array.isArray(x)&&Object.keys(x).sort().join()===k.slice().sort().join();
|
|
8
|
+
const birth=x=>exact(x,['platform','pid','start','boot'])&&x.platform==='linux'&&Number.isSafeInteger(x.pid)&&x.pid>0&&/^\d+$/.test(x.start)&&typeof x.boot==='string'&&/^[a-f0-9-]{36}$/.test(x.boot);
|
|
9
|
+
|
|
10
|
+
export function createReclamationWorker(api,q,j,{ledger,boundPlan,finish,bindWorker}){
|
|
11
|
+
const {path,hash,regular,verified,signed,atomicJSON,checkAncestors,runtimeAlive}=api;
|
|
12
|
+
const file=path.join(q.root,'.control/runtime/reclaim-worker.json'),binding=hash({operationId:q.operationId,runtimeId:j.id,inventoryHash:j.inventoryHash,workerHash:j.workerHash});
|
|
13
|
+
const fail=c=>api.fail('RUNTIME_RECLAIM_WORKER_'+c);
|
|
14
|
+
async function read(d){
|
|
15
|
+
await checkAncestors(path.dirname(file));const f=await regular(file,262144);if(!f){if(d?.workerBound)fail('REVIEW');return null;}
|
|
16
|
+
if(f.identity.uid!==process.getuid()||f.identity.mode!==0o600)fail('REVIEW');let x;try{x=verified(JSON.parse(f.data),q.proof);}catch{fail('REVIEW');}
|
|
17
|
+
if(!exact(x,['schema','binding','revision','launches'])||x.schema!==1||x.binding!==binding||!Number.isSafeInteger(x.revision)||x.revision<1||!Array.isArray(x.launches)||!x.launches.length||x.launches.length>128)fail('REVIEW');
|
|
18
|
+
const ids=new Set();for(const r of x.launches){
|
|
19
|
+
if(!exact(r,['id','reclaimId','planHash','identity','state','error'])||!uuid(r.id)||ids.has(r.id)||r.reclaimId!==d?.id||r.planHash!==d.planHash||!hex(r.planHash)||!['prepared','running','finished','failed'].includes(r.state)||r.identity!==null&&!birth(r.identity)||r.state!=='prepared'&&r.identity===null||r.error!==null&&(!/^ONBOARDING_CLOUD_[A-Z_]{1,160}$/.test(r.error)||r.state!=='failed'))fail('REVIEW');ids.add(r.id);
|
|
20
|
+
}return x;
|
|
21
|
+
}
|
|
22
|
+
async function save(x){x.revision++;if(Buffer.byteLength(JSON.stringify(x))>250000)fail('LIMIT');await atomicJSON(file,signed(x,q.proof));}
|
|
23
|
+
async function observe(d){
|
|
24
|
+
const x=await read(d),r=x?.launches.at(-1),active=r?.identity?await runtimeAlive(r.identity):false;
|
|
25
|
+
return {backgroundSupported:true,workerActive:active,executionState:!d?'not_requested':d.state==='committed'?'completed':active?'running':'interrupted',recoveryRequired:d?.state==='deleting'&&!active};
|
|
26
|
+
}
|
|
27
|
+
async function inactive(d){if((await observe(d)).workerActive)fail('ACTIVE');}
|
|
28
|
+
async function begin(d){
|
|
29
|
+
await inactive(d);const x=await read(d)??{schema:1,binding,revision:0,launches:[]};if(x.launches.length>=128)fail('LIMIT');
|
|
30
|
+
if(typeof api.runtimeReclaimLaunch!=='function'&&typeof api.reclaimWorkerSource!=='string'||typeof api.deferReclaimStart!=='function')fail('UNAVAILABLE');
|
|
31
|
+
const r={id:randomUUID(),reclaimId:d.id,planHash:d.planHash,identity:null,state:'prepared',error:null};x.launches.push(r);await save(x);if(!d.workerBound)await bindWorker(d);
|
|
32
|
+
const launch=api.runtimeReclaimLaunch??((...args)=>launchReclamationWorker({...api,processInfo:api.runtimeProcessInfo,source:api.reclaimWorkerSource},...args));
|
|
33
|
+
const handle=await launch(q,j,{id:r.id,planHash:d.planHash},async identity=>{
|
|
34
|
+
if(!birth(identity))fail('IDENTITY');r.identity=identity;r.state='running';await save(x);
|
|
35
|
+
});
|
|
36
|
+
// The dispatcher releases the original owner lock before delivering stdin.
|
|
37
|
+
// Parent loss before that point closes stdin: no unrecorded launch may execute.
|
|
38
|
+
api.deferReclaimStart(handle);return observe(d);
|
|
39
|
+
}
|
|
40
|
+
async function execute(a){
|
|
41
|
+
if(!exact(a,['id','planHash'])||!uuid(a.id)||!hex(a.planHash))fail('ARGUMENTS');
|
|
42
|
+
const d=await ledger(),x=await read(d),r=x?.launches.at(-1),self=await api.runtimeProcessInfo();
|
|
43
|
+
if(!d||d.state!=='deleting'||!r||r.id!==a.id||r.planHash!==a.planHash||r.state!=='running'||!birth(self)||hash(r.identity)!==hash(self))fail('NOT_AUTHORIZED');
|
|
44
|
+
try{const result=await finish(d,await boundPlan(d));r.state='finished';await save(x);return result;}
|
|
45
|
+
catch(e){r.state='failed';r.error=/^ONBOARDING_CLOUD_[A-Z_]{1,160}$/.test(e.message)?e.message:'ONBOARDING_CLOUD_RUNTIME_RECLAIM_WORKER_FAILED';await save(x);throw e;}
|
|
46
|
+
}
|
|
47
|
+
return {observe,inactive,begin,execute,fingerprint:async d=>hash({journal:d,worker:await read(d)})};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function launchReclamationWorker(api,q,j,args,persistIdentity){
|
|
51
|
+
const {fs,path,hash,regular,fail,runtimeDir,runtimeEnv,processInfo,source:originalProgram}=api;
|
|
52
|
+
const worker=path.join(runtimeDir(q),'worker.mjs'),source=await regular(worker,200000);
|
|
53
|
+
if(!source||source.identity.uid!==process.getuid()||source.identity.mode!==0o600||source.sha256!==j.workerHash||source.sha256!==hash(originalProgram))fail('RUNTIME_RECLAIM_WORKER_SOURCE_REVIEW');
|
|
54
|
+
const log=await fs.open(path.join(runtimeDir(q),'reclaim-'+args.id+'.log'),'wx',0o600);let child;
|
|
55
|
+
try{
|
|
56
|
+
child=runtimeSpawn(process.execPath,[worker],{cwd:q.root,detached:true,env:runtimeEnv(q),stdio:['pipe',log.fd,log.fd]});
|
|
57
|
+
child.stdin.on('error',()=>{});
|
|
58
|
+
await new Promise((resolve,reject)=>{child.once('spawn',resolve);child.once('error',reject);});
|
|
59
|
+
await persistIdentity(await processInfo(child.pid));child.unref();let sent=false;
|
|
60
|
+
const input=JSON.stringify({action:'runtime_version',root:q.root,operationId:q.operationId,installationId:q.installationId,proof:q.proof,planHash:q.planHash,versionAction:'reclaim_worker',args});
|
|
61
|
+
return {start:()=>{if(!sent){sent=true;child.stdin.end(input);}},cancel:()=>{if(!sent){sent=true;child.stdin.end();}}};
|
|
62
|
+
}catch(e){child?.stdin.end();child?.unref();throw e;}finally{await log.close();}
|
|
63
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Original-archive, offline runtime reclamation. Never adopts current user data.
|
|
2
|
+
// Requires completed three-artifact removal. Retains home/workspace/temp, source,
|
|
3
|
+
// every control receipt and backup. Deletion is irreversible and separately approved.
|
|
4
|
+
import {randomUUID} from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const retained=['runtime/native-home','runtime/native-tmp','runtime/workspace'];
|
|
7
|
+
const within=(p,root)=>p===root||p.startsWith(root+'/');
|
|
8
|
+
const keep=p=>p==='runtime'||retained.some(root=>within(p,root));
|
|
9
|
+
const uuid=x=>typeof x==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(x);
|
|
10
|
+
const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x);
|
|
11
|
+
const exact=(x,keys)=>x&&typeof x==='object'&&!Array.isArray(x)&&Object.keys(x).sort().join()===keys.slice().sort().join();
|
|
12
|
+
const identity=s=>({dev:s.dev,ino:s.ino,uid:s.uid,mode:s.mode&0o777});
|
|
13
|
+
const rowKeys=x=>['path','dev','ino','uid','mode','kind',...(x.kind==='file'?['bytes','sha256']:x.kind==='link'?['target']:[])];
|
|
14
|
+
|
|
15
|
+
export function createRuntimeReclamation(api,q,j,{prerequisite,background=false}){
|
|
16
|
+
const {fs,path,hash,fail,regular,signed,verified,atomicJSON,checkAncestors,runtimeSource,runtimeRecord,runtimeAlive,runtimeStopLedger,runtimeBaseInventory,runtimeInventory}=api;
|
|
17
|
+
const now=api.reclaimNow??Date.now,hook=api.reclaimHook??(async()=>{});
|
|
18
|
+
let workerPromise;const worker=()=>workerPromise??=import('./onboarding-runtime-reclaim-worker.js').then(m=>m.createReclamationWorker(api,q,j,{ledger,boundPlan,finish,bindWorker:d=>persist(d,{workerBound:true})}));
|
|
19
|
+
const result=async d=>({...view(d),...(background?await(await worker()).observe(d):{})});
|
|
20
|
+
const run=async(d,p)=>background?{...view(d),...await(await worker()).begin(d)}:finish(d,p);
|
|
21
|
+
const recoveryHash=async d=>background?(await worker()).fingerprint(d):hash(d);
|
|
22
|
+
const dir=path.join(q.root,'.control/runtime'),planFile=path.join(dir,'runtime-reclaim-plan.json'),journalFile=path.join(dir,'runtime-reclaim.json');
|
|
23
|
+
const binding=hash({root:q.root,operationId:q.operationId,runtimeId:j?.id,inventoryHash:j?.inventoryHash});
|
|
24
|
+
const bad=c=>fail('RUNTIME_RECLAIM_'+c);
|
|
25
|
+
async function stat(p){await checkAncestors(path.dirname(p));try{return await fs.lstat(p);}catch(e){if(e.code==='ENOENT')return null;throw e;}}
|
|
26
|
+
async function read(p,limit=67108864){await checkAncestors(path.dirname(p));const f=await regular(p,limit);if(!f)return null;if(f.identity.uid!==process.getuid()||f.identity.mode!==0o600)bad('LEDGER_REVIEW');try{return verified(JSON.parse(f.data),q.proof);}catch{bad('LEDGER_REVIEW');}}
|
|
27
|
+
async function save(p,v){if(Buffer.byteLength(JSON.stringify(v))>66000000)bad('LIMIT');await atomicJSON(p,signed(v,q.proof));}
|
|
28
|
+
async function guard(){
|
|
29
|
+
await runtimeSource(q);const current=await runtimeRecord(q);
|
|
30
|
+
if(!j?.inventoryHash||!j.nativeReceipt?.ok||!current||hash(current)!==hash(j)||!['host_ready','failed','stopped'].includes(j.state))bad('RUNTIME_CHANGED');
|
|
31
|
+
for(const member of [j.host,j.worker,j.child,j.restart?.launcher])if(await runtimeAlive(member))bad('PROCESS_ACTIVE');
|
|
32
|
+
await runtimeStopLedger(q,j,true);
|
|
33
|
+
}
|
|
34
|
+
function entries(rows){
|
|
35
|
+
if(!Array.isArray(rows)||rows.length>150000)bad('PLAN_REVIEW');const names=new Set();
|
|
36
|
+
for(const x of rows){
|
|
37
|
+
if(!x||!['file','directory','link'].includes(x.kind)||!exact(x,rowKeys(x))||typeof x.path!=='string'||!x.path.startsWith('runtime/')||keep(x.path)||within(x.path,'runtime/plugin')||x.path.split('/').some(s=>!s||s==='.'||s==='..'||/[\x00-\x1f\x7f\\]/.test(s))||names.has(x.path)||x.uid!==process.getuid()||![x.dev,x.ino,x.uid,x.mode].every(Number.isSafeInteger)||x.mode<0||x.mode>0o777)bad('PLAN_REVIEW');
|
|
38
|
+
if(x.kind==='file'&&(!Number.isSafeInteger(x.bytes)||x.bytes<0||x.bytes>268435456||!hex(x.sha256)))bad('PLAN_REVIEW');
|
|
39
|
+
if(x.kind==='link'&&(typeof x.target!=='string'||path.isAbsolute(x.target)||!within(path.posix.normalize(path.posix.join(path.posix.dirname(x.path),x.target)),'runtime')))bad('PLAN_REVIEW');
|
|
40
|
+
names.add(x.path);
|
|
41
|
+
}
|
|
42
|
+
// Every directory follows its children in deletion order.
|
|
43
|
+
const positions=new Map(rows.map((x,i)=>[x.path,i]));
|
|
44
|
+
for(const [i,x] of rows.entries()){const parent=path.posix.dirname(x.path);if(parent!=='runtime'&&(!positions.has(parent)||positions.get(parent)<=i||rows[positions.get(parent)].kind!=='directory'))bad('PLAN_REVIEW');}
|
|
45
|
+
return rows;
|
|
46
|
+
}
|
|
47
|
+
function planShape(p){
|
|
48
|
+
if(!p||!exact(p,['schema','binding','id','createdAt','expiresAt','journalHash','uninstallId','uninstallHash','rows','roots','fingerprint'])||p.schema!==1||p.binding!==binding||!uuid(p.id)||!uuid(p.uninstallId)||![p.journalHash,p.uninstallHash,p.fingerprint].every(hex)||!Number.isSafeInteger(p.createdAt)||p.expiresAt!==p.createdAt+300000||!Array.isArray(p.roots)||!p.roots.length)bad('PLAN_REVIEW');
|
|
49
|
+
const {fingerprint,...body}=p;if(hash(body)!==fingerprint||p.journalHash!==hash(j))bad('PLAN_REVIEW');entries(p.rows);
|
|
50
|
+
const names=new Set();for(const x of p.roots){if(!exact(x,['path','dev','ino','uid','mode','kind'])||!['runtime',...retained].includes(x.path)||x.kind!=='directory'||names.has(x.path)||x.uid!==process.getuid()||![x.dev,x.ino,x.uid,x.mode].every(Number.isSafeInteger)||x.mode<0||x.mode>0o777)bad('PLAN_REVIEW');names.add(x.path);}
|
|
51
|
+
if(!names.has('runtime'))bad('PLAN_REVIEW');return p;
|
|
52
|
+
}
|
|
53
|
+
async function ledger(){
|
|
54
|
+
const d=await read(journalFile,262144);if(!d)return null;
|
|
55
|
+
if(!exact(d,['schema','binding','id','fingerprint','planHash','state','revision','cursor','pendingEnd','total','updatedAt',...(background?['workerBound']:[])])||d.schema!==(background?2:1)||background&&typeof d.workerBound!=='boolean'||d.binding!==binding||!uuid(d.id)||![d.fingerprint,d.planHash].every(hex)||!['deleting','committed'].includes(d.state)||![d.revision,d.cursor,d.pendingEnd,d.total,d.updatedAt].every(Number.isSafeInteger)||d.revision<1||d.cursor<0||d.pendingEnd<d.cursor||d.pendingEnd>d.total||d.pendingEnd-d.cursor>32||d.total<1||d.total>150000||d.state==='committed'&&(d.cursor!==d.total||d.pendingEnd!==d.total))bad('LEDGER_REVIEW');
|
|
56
|
+
return d;
|
|
57
|
+
}
|
|
58
|
+
async function boundPlan(d){const p=planShape(await read(planFile));if(p.id!==d.id||p.fingerprint!==d.fingerprint||hash(p)!==d.planHash||p.rows.length!==d.total)bad('PLAN_REVIEW');return p;}
|
|
59
|
+
async function pinnedRoots(p){
|
|
60
|
+
for(const row of p.roots){const s=await stat(path.join(q.root,row.path));if(!s||!s.isDirectory()||s.isSymbolicLink()||hash(identity(s))!==hash({dev:row.dev,ino:row.ino,uid:row.uid,mode:row.mode}))bad('RETAINED_ROOT_DRIFT');}
|
|
61
|
+
// Never follow a newly substituted retained root, even though its data is kept.
|
|
62
|
+
for(const name of retained){const s=await stat(path.join(q.root,name));if(s&&(!s.isDirectory()||s.isSymbolicLink()||!p.roots.some(x=>x.path===name)))bad('RETAINED_ROOT_DRIFT');}
|
|
63
|
+
}
|
|
64
|
+
async function snapshot(){return runtimeInventory(q,new Set(retained));}
|
|
65
|
+
function compare(expected,current,cursor,end){
|
|
66
|
+
const found=new Map(current.filter(x=>!keep(x.path)).map(x=>[x.path,x]));
|
|
67
|
+
for(const [i,row] of expected.entries()){const x=found.get(row.path);found.delete(row.path);if(i<cursor?!!x:i>=end?!x||hash(x)!==hash(row):x&&hash(x)!==hash(row))bad('DRIFT');}
|
|
68
|
+
if(found.size)bad('DRIFT');
|
|
69
|
+
}
|
|
70
|
+
async function validate(d,p){await guard();if(hash(await prerequisite())!==p.uninstallHash)bad('PREREQUISITE_CHANGED');await pinnedRoots(p);compare(p.rows,await snapshot(),d.cursor,d.pendingEnd);}
|
|
71
|
+
const view=d=>({contract:1,operationId:q.operationId,runtimeId:j.id,scope:'stopped_owned_runtime_keep_user_data',id:d?.id??null,state:d?.state??'not_requested',completed:d?.cursor??0,total:d?.total??0,recoveryRequired:d?.state==='deleting',managedRestartBlocked:!!d,hostStarted:false,initialization:false,automaticRetry:false,userDataPreserved:true,backupsRetained:true,userDataDeleted:false,runtimeFilesRemoved:d?.state==='committed',irreversible:true});
|
|
72
|
+
function publicPlan(p){return {...view(null),id:p.id,fingerprint:p.fingerprint,expiresAt:p.expiresAt,canApply:true,files:p.rows.filter(x=>x.kind==='file').length,entries:p.rows.length,bytes:p.rows.reduce((n,x)=>n+(x.bytes??0),0),removeRoots:[...new Set(p.rows.map(x=>x.path.split('/').slice(0,2).join('/')))].sort(),retainedPaths:[...retained,'runtime','.control','plugin'],requiresExplicitConfirmation:true};}
|
|
73
|
+
async function preview(a={}){
|
|
74
|
+
if(!exact(a,[]))bad('ARGUMENTS');await guard();if(await ledger())bad('ALREADY_REQUESTED');
|
|
75
|
+
const u=await prerequisite();if(u?.state!=='committed'||u.pluginInstalled!==false||u.recoveryRequired||!uuid(u.id))bad('PLUGIN_UNINSTALL_REQUIRED');
|
|
76
|
+
const original=await runtimeBaseInventory(q,j),rows=entries(original.items.filter(x=>!keep(x.path)&&!within(x.path,'runtime/plugin')).reverse());
|
|
77
|
+
if(!rows.length)bad('NOTHING_OWNED');const roots=original.items.filter(x=>['runtime',...retained].includes(x.path));
|
|
78
|
+
const createdAt=now(),body={schema:1,binding,id:randomUUID(),createdAt,expiresAt:createdAt+300000,journalHash:hash(j),uninstallId:u.id,uninstallHash:hash(u),rows,roots};
|
|
79
|
+
const p=planShape({...body,fingerprint:hash(body)});await pinnedRoots(p);compare(rows,await snapshot(),0,0);
|
|
80
|
+
await save(planFile,p);return publicPlan(p);
|
|
81
|
+
}
|
|
82
|
+
async function syncParents(rows){
|
|
83
|
+
const names=new Set();for(const row of rows){let n=path.dirname(path.join(q.root,row.path));while(!await stat(n)){if(n===q.root)bad('DRIFT');n=path.dirname(n);}names.add(n);}
|
|
84
|
+
for(const n of names){const h=await fs.open(n,'r');try{await h.sync();}finally{await h.close();}}
|
|
85
|
+
}
|
|
86
|
+
async function remove(row,parents){
|
|
87
|
+
const name=path.join(q.root,row.path),s=await stat(name);if(!s)return;
|
|
88
|
+
for(let parent=path.posix.dirname(row.path);parent!=='runtime';parent=path.posix.dirname(parent)){
|
|
89
|
+
const expected=parents.get(parent),actual=await stat(path.join(q.root,parent));
|
|
90
|
+
if(!expected||!actual||!actual.isDirectory()||actual.isSymbolicLink()||hash(identity(actual))!==hash(identityRow(expected)))bad('DRIFT');
|
|
91
|
+
}
|
|
92
|
+
if(hash(identity(s))!==hash({dev:row.dev,ino:row.ino,uid:row.uid,mode:row.mode}))bad('DRIFT');
|
|
93
|
+
if(row.kind==='directory'){if(!s.isDirectory()||s.isSymbolicLink())bad('DRIFT');await fs.rmdir(name);}
|
|
94
|
+
else if(row.kind==='link'){if(!s.isSymbolicLink()||await fs.readlink(name)!==row.target)bad('DRIFT');await fs.unlink(name);}
|
|
95
|
+
else{const f=await regular(name,268435456);if(!f||f.sha256!==row.sha256||s.size!==row.bytes||hash(f.identity)!==hash(identityRow(row)))bad('DRIFT');const latest=await stat(name);if(!latest||latest.nlink!==1||hash(identity(latest))!==hash(identityRow(row)))bad('DRIFT');await fs.unlink(name);}
|
|
96
|
+
}
|
|
97
|
+
const identityRow=x=>({dev:x.dev,ino:x.ino,uid:x.uid,mode:x.mode});
|
|
98
|
+
async function persist(d,fields){Object.assign(d,fields,{revision:d.revision+1,updatedAt:now()});await save(journalFile,d);}
|
|
99
|
+
async function finish(d,p){
|
|
100
|
+
await validate(d,p);const parents=new Map(p.rows.filter(x=>x.kind==='directory').map(x=>[x.path,x]));
|
|
101
|
+
while(d.cursor<d.total){
|
|
102
|
+
await guard();await pinnedRoots(p);const start=d.cursor;await persist(d,{pendingEnd:Math.min(start+32,d.total)});await hook('batch_intent',d);
|
|
103
|
+
for(let i=start;i<d.pendingEnd;i++){await remove(p.rows[i],parents);await hook('item_removed',{index:i,path:p.rows[i].path});}
|
|
104
|
+
await syncParents(p.rows.slice(start,d.pendingEnd));await persist(d,{cursor:d.pendingEnd});await hook('batch_recorded',d);
|
|
105
|
+
}
|
|
106
|
+
await validate(d,p);await persist(d,{state:'committed'});await hook('committed',d);return view(d);
|
|
107
|
+
}
|
|
108
|
+
async function apply(a){
|
|
109
|
+
if(!exact(a,['id','fingerprint','confirm'])||!uuid(a.id)||!hex(a.fingerprint)||a.confirm!==true)bad('CONFIRMATION_REQUIRED');
|
|
110
|
+
await guard();const old=await ledger();if(old){if(old.id!==a.id||old.fingerprint!==a.fingerprint)bad('ALREADY_REQUESTED');return {...await result(old),notResent:true};}
|
|
111
|
+
const p=planShape(await read(planFile));if(p.id!==a.id||p.fingerprint!==a.fingerprint||now()>p.expiresAt||now()<p.createdAt)bad('PLAN_EXPIRED');
|
|
112
|
+
const u=await prerequisite();if(hash(u)!==p.uninstallHash)bad('PREREQUISITE_CHANGED');
|
|
113
|
+
const d={schema:background?2:1,...(background?{workerBound:false}:{}),binding,id:p.id,fingerprint:p.fingerprint,planHash:hash(p),state:'deleting',revision:1,cursor:0,pendingEnd:0,total:p.rows.length,updatedAt:now()};
|
|
114
|
+
await validate(d,p);await save(journalFile,d);await hook('intent_saved',d);return run(d,p);
|
|
115
|
+
}
|
|
116
|
+
async function status(a={}){if(!exact(a,[]))bad('ARGUMENTS');const d=await ledger();if(d)await boundPlan(d);return {...await result(d),historical:true};}
|
|
117
|
+
function intent(p){return {schema:background?2:1,...(background?{workerBound:false}:{}),binding,id:p.id,fingerprint:p.fingerprint,planHash:hash(p),state:'deleting',revision:1,cursor:0,pendingEnd:0,total:p.rows.length,updatedAt:now()};}
|
|
118
|
+
const unobserved=p=>hash({state:'intent_unobserved',planHash:hash(p)});
|
|
119
|
+
async function recoveryPreview(a={}){
|
|
120
|
+
if(!exact(a,[]))bad('ARGUMENTS');const d=await ledger();
|
|
121
|
+
if(!d){const p=planShape(await read(planFile));await validate(intent(p),p);return {...view(null),id:p.id,state:'unobserved',recoveryRequired:true,managedRestartBlocked:true,total:p.rows.length,fingerprint:unobserved(p),action:'start_original_manifest_if_still_unobserved',requiresExplicitConfirmation:true};}
|
|
122
|
+
if(d.state!=='deleting')bad('RECOVERY_REQUIRED');if(background)await(await worker()).inactive(d);const p=await boundPlan(d);await validate(d,p);
|
|
123
|
+
return {...await result(d),fingerprint:await recoveryHash(d),action:'continue_original_manifest_only',requiresExplicitConfirmation:true};
|
|
124
|
+
}
|
|
125
|
+
async function recover(a){
|
|
126
|
+
if(!exact(a,['id','fingerprint','confirm'])||a.confirm!==true||!uuid(a.id)||!hex(a.fingerprint))bad('CONFIRMATION_REQUIRED');
|
|
127
|
+
const d=await ledger();
|
|
128
|
+
if(!d){const p=planShape(await read(planFile));if(p.id!==a.id||unobserved(p)!==a.fingerprint)bad('RECOVERY_CHANGED');const next=intent(p);await validate(next,p);await save(journalFile,next);await hook('intent_saved',next);return run(next,p);}
|
|
129
|
+
if(d.state!=='deleting'||d.id!==a.id||await recoveryHash(d)!==a.fingerprint)bad('RECOVERY_CHANGED');if(background)await(await worker()).inactive(d);const p=await boundPlan(d);await validate(d,p);return run(d,p);
|
|
130
|
+
}
|
|
131
|
+
return {preview,apply,status,recoveryPreview,recover,worker:async a=>{if(!background)bad('WORKER_UNSUPPORTED');return(await worker()).execute(a);}};
|
|
132
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Local durable intent is separate from the native runtime status receipt.
|
|
2
|
+
import {credentialKey} from '@deepseek-ai/dsh-credentials';
|
|
3
|
+
import {runtimeProgram,runtimeCommand} from './onboarding-runtime-program.js';
|
|
4
|
+
export const repairRecordKey=id=>credentialKey('dsh-task-gateway','onboarding-runtime-repair-v1-'+id);
|
|
5
|
+
export const repairPending=p=>!!p&&(p.state==='requested'||p.receipt?.repair?.recoveryRequired===true);
|
|
6
|
+
const uuid=x=>typeof x==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(x);
|
|
7
|
+
const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x);
|
|
8
|
+
const exact=(x,k)=>x&&Object.keys(x).sort().join()===k.slice().sort().join();
|
|
9
|
+
const fail=c=>{throw Error('ONBOARDING_CLOUD_RUNTIME_REPAIR_'+c);};
|
|
10
|
+
export function parseRepairRequest(r,operationId){
|
|
11
|
+
if(!r)return null;const p=r.payload;
|
|
12
|
+
if(r.kind!=='grant'||!exact(p,['schema','operationId','id','action','fingerprint','state','receipt'])||p.schema!==1||p.operationId!==operationId||!uuid(p.id)||!['runtime_repair_apply','runtime_repair_recover'].includes(p.action)||p.fingerprint!==null&&!hex(p.fingerprint)||!['requested','observed'].includes(p.state))fail('LOCAL_LEDGER_REVIEW');if(p.state==='requested'?p.receipt!==null:(!p.receipt||typeof p.receipt.state!=='string'||p.receipt.repair?.id!==p.id||typeof p.receipt.repair?.recoveryRequired!=='boolean'))fail('LOCAL_LEDGER_REVIEW');return p;
|
|
13
|
+
}
|
|
14
|
+
export async function runtimeRepairClient({args,p,a,b,credentials,ssh}){
|
|
15
|
+
const action=args?.action,verifying=action==='runtime_repair_verify',mutating=['runtime_repair_apply','runtime_repair_recover'].includes(action);
|
|
16
|
+
if(!['runtime_repair_preview','runtime_repair_status','runtime_repair_apply','runtime_repair_recover','runtime_repair_verify'].includes(action)||!exact(args,['action','operationId',...(mutating?['repairId','confirm']:verifying?['repairId']:[]),...(action==='runtime_repair_apply'?['fingerprint']:[])])||args.operationId!==p.id||mutating&&(!uuid(args.repairId)||args.confirm!==true)||verifying&&!uuid(args.repairId)||action==='runtime_repair_apply'&&!hex(args.fingerprint))fail('CONFIRMATION_REQUIRED');
|
|
17
|
+
const compat=a.files.find(f=>f.path==='plugin/compatibility.json');if(!compat||JSON.parse(Buffer.from(compat.data,'base64')).onboardingPluginRepair!==1)fail('UNSUPPORTED');
|
|
18
|
+
if(verifying&&JSON.parse(Buffer.from(compat.data,'base64')).onboardingPluginRepairActivation!==1)fail('ACTIVATION_UNSUPPORTED');
|
|
19
|
+
const controller=a.files.find(f=>f.path==='plugin/onboarding-remote-control.js'),extension=a.files.find(f=>f.path==='plugin/onboarding-runtime-extension.js');if(!controller||!extension)fail('ARCHIVE_REVIEW');
|
|
20
|
+
const key=repairRecordKey(p.id),c=credentials(),old=parseRepairRequest(await c.readRecord(key),p.id);
|
|
21
|
+
if(mutating&&old?.id===args.repairId&&old.action===action&&(action==='runtime_repair_apply'||old.state==='requested')){
|
|
22
|
+
if(old.fingerprint!==(args.fingerprint??null))fail('PLAN_DRIFT');return {operationId:p.id,initialization:false,hostStarted:false,state:old.state==='requested'?'repair_outcome_unknown':old.receipt?.state??'repair_outcome_unknown',repair:old.receipt?.repair??null,notResent:true,cached:true};
|
|
23
|
+
}
|
|
24
|
+
if(repairPending(old)&&(verifying||['runtime_repair_preview','runtime_repair_apply'].includes(action)||action==='runtime_repair_recover'&&old.id!==args.repairId))fail('RECOVERY_REQUIRED');
|
|
25
|
+
const q={action,root:p.root,operationId:p.id,installationId:p.installationId,proof:p.proof,planHash:p.planHash,...(mutating||verifying?{repairId:args.repairId}:{}),...(action==='runtime_repair_apply'?{fingerprint:args.fingerprint}:{})};
|
|
26
|
+
const program=runtimeProgram(Buffer.from(controller.data,'base64').toString(),Buffer.from(extension.data,'base64').toString());
|
|
27
|
+
const command=runtimeCommand(a.baseline.node.path,program,'runtimeCLI');
|
|
28
|
+
let request=old;
|
|
29
|
+
if(mutating){
|
|
30
|
+
request={schema:1,operationId:p.id,id:args.repairId,action,fingerprint:args.fingerprint??null,state:'requested',receipt:null};
|
|
31
|
+
await c.modifyRecord(key,r=>{if(JSON.stringify(parseRepairRequest(r,p.id))!==JSON.stringify(old))fail('LOCAL_CONFLICT');return {kind:'grant',payload:request};});
|
|
32
|
+
}
|
|
33
|
+
let result;try{
|
|
34
|
+
const raw=await ssh.execute(b,command,JSON.stringify(q),{timeoutMs:120000});
|
|
35
|
+
result=JSON.parse(raw);
|
|
36
|
+
}catch{fail('OUTCOME_UNKNOWN');}
|
|
37
|
+
if(!result.ok){
|
|
38
|
+
if(mutating&&result.error==='ONBOARDING_CLOUD_RUNTIME_REPAIR_PREFLIGHT_REFUSED')await c.modifyRecord(key,r=>{const cur=parseRepairRequest(r,p.id);if(JSON.stringify(cur)!==JSON.stringify(request))fail('LOCAL_CONFLICT');return {kind:'grant',payload:{...request,state:'observed',receipt:{state:'repair_refused',repair:{id:request.id,state:'refused',recoveryRequired:false}}}};});
|
|
39
|
+
throw Error(/^ONBOARDING_CLOUD_[A-Z_]+$/.test(result.error)?result.error:'ONBOARDING_CLOUD_RUNTIME_REPAIR_RESPONSE_REVIEW');
|
|
40
|
+
}
|
|
41
|
+
const v=result.value;
|
|
42
|
+
if(!v||v.operationId!==p.id||!uuid(v.runtimeId)||v.initialization!==false||typeof v.hostStarted!=='boolean'||action==='runtime_repair_preview'&&(!uuid(v.id)||!hex(v.fingerprint)||typeof v.canApply!=='boolean')||action!=='runtime_repair_preview'&&(!v.repair||v.repair.contract!==1||typeof v.repair.recoveryRequired!=='boolean'))fail('RESPONSE_REVIEW');
|
|
43
|
+
if(verifying&&(v.repair.id!==args.repairId||v.repair.state!=='committed'||v.activation?.contract!==1||v.activation.activationVerified!==true||v.activation.cached!==false||v.activation.repairId!==args.repairId||!Number.isSafeInteger(v.activation.observedAt)))fail('RESPONSE_REVIEW');
|
|
44
|
+
if(request&&action!=='runtime_repair_preview'){
|
|
45
|
+
const matched=v.repair.id===request.id;
|
|
46
|
+
await c.modifyRecord(key,r=>{if(JSON.stringify(parseRepairRequest(r,p.id))!==JSON.stringify(request))fail('LOCAL_CONFLICT');return {kind:'grant',payload:{...request,state:matched?'observed':request.state,receipt:matched?v:request.receipt}};});
|
|
47
|
+
if(!matched&&request.state==='requested')return {...v,state:'repair_outcome_unknown',originalRepairId:request.id,notResent:true};
|
|
48
|
+
}
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Loaded only from the original verified archive, under the runtime owner lock.
|
|
2
|
+
// Replaces only original plugin files. Never rebases a whole filesystem scan.
|
|
3
|
+
import {createPluginRepair} from './onboarding-plugin-repair.js';
|
|
4
|
+
import {createRuntimeActivation} from './onboarding-runtime-activation.js';
|
|
5
|
+
export async function createRuntimeRepair(api,q,j){
|
|
6
|
+
const {fs,path,hash,fail,regular,verified,signed,atomicJSON,runtimeOwnedInventory,runtimeInventory,runtimeAlive,runtimeRecord,runtimeStopLedger,runtimeSource}=api;
|
|
7
|
+
if(!j)fail('RUNTIME_REPAIR_NOT_READY');const c=JSON.parse((await regular(path.join(q.root,'plugin/compatibility.json'))).data);if(c.onboardingPluginRepair!==1)fail('RUNTIME_REPAIR_UNSUPPORTED');
|
|
8
|
+
const source=await runtimeSource(q),original=await runtimeOwnedInventory(q,j),file=path.join(q.root,'.control/runtime/plugin-maintenance.json');
|
|
9
|
+
const manifest=source.owner.manifest.map(f=>({path:f.path.slice(7),sha256:f.sha256}));
|
|
10
|
+
const sources=new Map(source.owner.manifest.map(f=>['runtime/'+f.path,f]));
|
|
11
|
+
const mutable=p=>['runtime/native-home','runtime/native-tmp','runtime/workspace'].some(x=>p===x||p.startsWith(x+'/'));
|
|
12
|
+
const terminal=r=>['committed','rolled_back'].includes(r.state);
|
|
13
|
+
const scope='stopped_owned_plugin_files_only';
|
|
14
|
+
const exact=(x,ks)=>x&&Object.keys(x).sort().join()===ks.slice().sort().join();
|
|
15
|
+
const uuid=x=>typeof x==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(x);
|
|
16
|
+
const hex=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x);
|
|
17
|
+
async function ledger(){
|
|
18
|
+
let folder;try{folder=await fs.lstat(path.join(q.root,'.control/runtime/plugin-repairs'));}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
19
|
+
if(folder&&(!folder.isDirectory()||folder.isSymbolicLink()||folder.uid!==process.getuid()||(folder.mode&0o777)!==0o700))fail('RUNTIME_REPAIR_LEDGER_REVIEW');
|
|
20
|
+
const names=folder?await fs.readdir(path.join(q.root,'.control/runtime/plugin-repairs')):[];
|
|
21
|
+
const f=await regular(file,16777216);if(!f&&folder)fail('RUNTIME_REPAIR_LEDGER_REVIEW');if(!f)return {schema:1,operationId:q.operationId,runtimeId:j.id,inventoryHash:j.inventoryHash,records:[]};
|
|
22
|
+
if(f.identity.uid!==process.getuid()||f.identity.mode!==0o600)fail('RUNTIME_REPAIR_LEDGER_REVIEW');
|
|
23
|
+
const d=verified(JSON.parse(f.data),q.proof);
|
|
24
|
+
if(!exact(d,['schema','operationId','runtimeId','inventoryHash','records'])||d.schema!==1||d.operationId!==q.operationId||d.runtimeId!==j.id||d.inventoryHash!==j.inventoryHash||!Array.isArray(d.records)||d.records.length>128||new Set(d.records.map(x=>x.id)).size!==d.records.length)fail('RUNTIME_REPAIR_LEDGER_REVIEW');
|
|
25
|
+
for(const r of d.records)if(!exact(r,['id','fingerprint','baseHash','state','delta','updatedAt'])||!uuid(r.id)||!hex(r.fingerprint)||!hex(r.baseHash)||!['applying','committed','rolled_back','needs_review'].includes(r.state)||!Array.isArray(r.delta)||r.delta.length>512||!Number.isSafeInteger(r.updatedAt)||r.state!=='committed'&&r.delta.length)fail('RUNTIME_REPAIR_LEDGER_REVIEW');
|
|
26
|
+
if(names.some(n=>!d.records.some(r=>r.id===n)))fail('RUNTIME_REPAIR_LEDGER_REVIEW');
|
|
27
|
+
if(d.records.slice(0,-1).some(x=>!terminal(x)))fail('RUNTIME_REPAIR_LEDGER_REVIEW');return d;
|
|
28
|
+
}
|
|
29
|
+
async function save(d){const value=signed(d,q.proof);if(Buffer.byteLength(JSON.stringify(value))>16777216)fail('RUNTIME_REPAIR_HISTORY_LIMIT');await atomicJSON(file,value);}
|
|
30
|
+
function merged(d,requireTerminal=false){
|
|
31
|
+
const map=new Map(original.items.map(x=>[x.path,x]));
|
|
32
|
+
for(const r of d.records){
|
|
33
|
+
if(r.baseHash!==hash([...map.values()]))fail('RUNTIME_REPAIR_OWNERSHIP_REVIEW');
|
|
34
|
+
if(!terminal(r)&&requireTerminal)fail('RUNTIME_REPAIR_RECOVERY_REQUIRED');
|
|
35
|
+
const seen=new Set();
|
|
36
|
+
for(const delta of r.delta){const e=delta.after,f=sources.get(e?.path),old=map.get(e?.path);
|
|
37
|
+
if(!exact(delta,['before','after'])||!f||seen.has(e.path)||!old||hash(delta.before)!==hash(old)||!exact(e,['path','kind','dev','ino','uid','mode','bytes','sha256'])||e.kind!=='file'||![e.dev,e.ino,e.uid,e.bytes].every(Number.isSafeInteger)||e.uid!==process.getuid()||e.mode!==0o600||e.sha256!==f.sha256||e.bytes!==f.bytes)fail('RUNTIME_REPAIR_OWNERSHIP_REVIEW');
|
|
38
|
+
seen.add(e.path);map.set(e.path,e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...map.values()];
|
|
42
|
+
}
|
|
43
|
+
async function stopped(){
|
|
44
|
+
const current=await runtimeRecord(q);
|
|
45
|
+
if(current?.id!==j.id||current.inventoryHash!==j.inventoryHash||current.revision!==j.revision)fail('RUNTIME_REPAIR_RUNTIME_CHANGED');
|
|
46
|
+
if(!current.nativeReceipt?.ok||!['host_ready','failed','stopped'].includes(current.state))fail('RUNTIME_REPAIR_NOT_READY');
|
|
47
|
+
for(const p of [current.host,current.worker,current.child,current.restart?.launcher])if(await runtimeAlive(p))fail('RUNTIME_PROCESS_ACTIVE');
|
|
48
|
+
await runtimeStopLedger(q,current,true);
|
|
49
|
+
}
|
|
50
|
+
async function outside(d){
|
|
51
|
+
await stopped();const expected=merged(d),omit=new Set(sources.keys()),current=await runtimeInventory(q,omit);
|
|
52
|
+
const filter=x=>!mutable(x.path)&&!omit.has(x.path);
|
|
53
|
+
if(hash(current.filter(filter))!==hash(expected.filter(filter)))fail('RUNTIME_REPAIR_OUTSIDE_DRIFT');
|
|
54
|
+
}
|
|
55
|
+
const inventory=original.items.filter(x=>x.path.startsWith('runtime/plugin/')).map(x=>({path:x.path.slice(15),kind:x.kind,identity:{dev:x.dev,ino:x.ino,mode:x.mode},sha256:x.sha256??null}));
|
|
56
|
+
const repair=createPluginRepair({root:q.root,operationId:q.operationId,runtimeId:j.id,proof:q.proof,manifest,inventory,exclusive:fn=>fn(),verifyOwnedStopped:async()=>{await stopped();return {operationId:q.operationId,runtimeId:j.id,stopped:true,ownershipVerified:true};}});
|
|
57
|
+
function view(d,r=d.records.at(-1)){return {operationId:q.operationId,runtimeId:j.id,initialization:false,hostStarted:false,state:r?'repair_'+r.state:'repair_not_started',scope,repair:{contract:1,id:r?.id??null,state:r?.state??'not_started',recoveryRequired:!!r&&!terminal(r),ownershipRebased:r?.state==='committed',activationVerified:false,versionUpgrade:false,files:r?.delta.map(x=>x.after.path.slice(15))??[]},automaticRetry:false};}
|
|
58
|
+
async function finish(d,r,result){
|
|
59
|
+
await outside(d);
|
|
60
|
+
if(result.state==='committed'){
|
|
61
|
+
const current=merged(d),map=new Map(current.map(x=>[x.path,x])),rows=await repair.ownership({id:r.id});
|
|
62
|
+
r.delta=rows.map(x=>{const name='runtime/plugin/'+x.path,before=map.get(name);if(!before)fail('RUNTIME_REPAIR_OWNERSHIP_REVIEW');return {before,after:{path:name,dev:x.identity.dev,ino:x.identity.ino,uid:process.getuid(),mode:x.identity.mode,kind:'file',bytes:x.bytes,sha256:x.sha256}};});
|
|
63
|
+
}
|
|
64
|
+
r.state=result.state;r.updatedAt=Date.now();merged(d);await save(d);return view(d);
|
|
65
|
+
}
|
|
66
|
+
async function action(name,args){
|
|
67
|
+
const d=await ledger();merged(d);
|
|
68
|
+
const activation=()=>createRuntimeActivation(api,q,j,{repair:d.records.at(-1),inventory:merged(d,true),source,compat:c});
|
|
69
|
+
if(name==='verify'){if(c.onboardingPluginRepairActivation!==1)fail('RUNTIME_ACTIVATION_UNSUPPORTED');return {...view(d),activation:await activation().verify(args.id)};}
|
|
70
|
+
if(name==='status')return {...view(d),activation:c.onboardingPluginRepairActivation===1?await createRuntimeActivation(api,q,j,{repair:d.records.at(-1),inventory:[],source,compat:c}).status():null};
|
|
71
|
+
try{await outside(d);}catch(e){if(name==='apply'&&!d.records.some(x=>x.id===args.id))fail('RUNTIME_REPAIR_PREFLIGHT_REFUSED');throw e;}
|
|
72
|
+
if(name==='preview'){if(d.records.some(x=>!terminal(x)))fail('RUNTIME_REPAIR_RECOVERY_REQUIRED');const p=await repair.preview();return {...p,operationId:q.operationId,runtimeId:j.id,initialization:false,repairSupported:true,ownershipRebased:false,activationVerified:false};}
|
|
73
|
+
if(name==='apply'){
|
|
74
|
+
const old=d.records.find(x=>x.id===args.id);if(old){if(old.fingerprint!==args.fingerprint)fail('RUNTIME_REPAIR_PLAN_DRIFT');return {...view(d,old),notResent:true,cached:true};}
|
|
75
|
+
if(d.records.some(x=>!terminal(x)))fail('RUNTIME_REPAIR_RECOVERY_REQUIRED');if(d.records.length>=128)fail('RUNTIME_REPAIR_HISTORY_LIMIT');
|
|
76
|
+
try{await repair.validate(args);}catch{fail('RUNTIME_REPAIR_PREFLIGHT_REFUSED');}const r={id:args.id,fingerprint:args.fingerprint,baseHash:hash(merged(d)),state:'applying',delta:[],updatedAt:Date.now()};d.records.push(r);await save(d);
|
|
77
|
+
// Unknown publication is never retried. Recovery examines the same signed journal.
|
|
78
|
+
try{return await finish(d,r,await repair.apply(args));}catch(e){r.state='needs_review';r.delta=[];r.updatedAt=Date.now();await save(d);throw e;}
|
|
79
|
+
}
|
|
80
|
+
if(name==='recover'){
|
|
81
|
+
const r=d.records.at(-1);if(!r||r.id!==args.id)fail('RUNTIME_REPAIR_RECOVERY_REQUIRED');if(terminal(r))return {...view(d),notResent:true};
|
|
82
|
+
return finish(d,r,await repair.recover(args));
|
|
83
|
+
}
|
|
84
|
+
fail('ARGUMENTS');
|
|
85
|
+
}
|
|
86
|
+
async function dispatch(){try{const v=await action(q.action.slice(15),{...(q.repairId?{id:q.repairId,confirm:true}:{}),...(q.fingerprint?{fingerprint:q.fingerprint}:{})});return {...v,hostStarted:await runtimeAlive(j.host)};}catch(e){if(/^ONBOARDING_PLUGIN_REPAIR_[A-Z_]+$/.test(e.message))fail('RUNTIME_REPAIR_'+e.message.slice(25));throw e;}}
|
|
87
|
+
return {action,dispatch,effective:async()=>merged(await ledger(),true)};
|
|
88
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {observeRuntimeHost} from './onboarding-host-stop-observation.js';
|
|
2
|
+
// Fixed archived stop controller. Native Host shutdown needs an independent
|
|
3
|
+
// risk-confirmed schema-2 plan; an idle snapshot is never a lossless drain.
|
|
4
|
+
export function createRuntimeStop(api){
|
|
5
|
+
const {fs,path,randomUUID,runtimeDir,regular,verified,signed,exact,uuid,hash,fail,runtimeAlive,runtimeRecord,runtimeView,atomicJSON}=api;
|
|
6
|
+
// Stop authorization is separate from the worker journal, which the installer
|
|
7
|
+
// may update concurrently. Legacy installer plans never authorize a Host signal.
|
|
8
|
+
const observeHost=(q,j)=>observeRuntimeHost(api,q,j);
|
|
9
|
+
const hostScope='owned_host_with_loss_risk';
|
|
10
|
+
const risk={contract:1,lateNativeInputMayAbort:true,imInFlightMayAbort:true,ownQueuesRetained:true,ingressFrozen:false,automaticStop:false,requiresExplicitAcknowledgement:true};
|
|
11
|
+
const stopPath=q=>path.join(runtimeDir(q),'stop.json');
|
|
12
|
+
async function runtimeStopLedger(q,j,terminalOnly=false){
|
|
13
|
+
const r=await regular(stopPath(q),262144);if(!r)return {schema:1,operationId:q.operationId,runtimeId:j.id,records:[]};
|
|
14
|
+
if(r.identity.mode!==0o600||r.identity.uid!==process.getuid())fail('RUNTIME_STOP_LEDGER_REVIEW');
|
|
15
|
+
const d=verified(JSON.parse(r.data),q.proof);
|
|
16
|
+
if(!exact(d,['schema','operationId','runtimeId','records'])||d.schema!==1||d.operationId!==q.operationId||d.runtimeId!==j.id||!Array.isArray(d.records)||d.records.length>32)fail('RUNTIME_STOP_LEDGER_REVIEW');
|
|
17
|
+
const ids=new Set();for(const row of d.records){
|
|
18
|
+
if(!exact(row,['plan','phase','updatedAt','error'])||!['requested','signalled','needs_review','ended'].includes(row.phase)||!Number.isSafeInteger(row.updatedAt)||row.error!==null&&!/^ONBOARDING_CLOUD_[A-Z_]+$/.test(row.error))fail('RUNTIME_STOP_LEDGER_REVIEW');
|
|
19
|
+
const p=runtimeStopPlan(q,j,row.plan);if(ids.has(p.id))fail('RUNTIME_STOP_LEDGER_REVIEW');ids.add(p.id);
|
|
20
|
+
if(terminalOnly&&row.phase!=='ended')fail('RUNTIME_STOP_RECONCILE_REQUIRED');
|
|
21
|
+
}
|
|
22
|
+
return d;
|
|
23
|
+
}
|
|
24
|
+
function runtimeStopPlan(q,j,value){
|
|
25
|
+
const p=verified(value,q.proof);
|
|
26
|
+
const host=p.schema===2;
|
|
27
|
+
if(!exact(p,['schema','id','operationId','runtimeId','revision','scope','createdAt','expiresAt','members','host','canApply','reason',...(host?['risk','observation']:[])])||![1,2].includes(p.schema)||!uuid(p.id)||p.operationId!==q.operationId||p.runtimeId!==j.id||!Number.isSafeInteger(p.revision)||p.revision<1||p.scope!==(host?hostScope:'installer_processes_only')||!Number.isSafeInteger(p.createdAt)||p.expiresAt!==p.createdAt+300000||!exact(p.members,['worker','child','launcher'])||typeof p.canApply!=='boolean'||!['ready','no_active_installer','host_drain_required','prior_stop_unresolved','host_busy','host_observation_required','installer_active'].includes(p.reason))fail('RUNTIME_STOP_PLAN_REVIEW');
|
|
28
|
+
for(const m of [...Object.values(p.members),p.host])if(m!==null&&(!exact(m,['platform','pid','boot','start'])||m.platform!=='linux'||!Number.isSafeInteger(m.pid)||m.pid<2||typeof m.boot!=='string'||typeof m.start!=='string'))fail('RUNTIME_STOP_PLAN_REVIEW');
|
|
29
|
+
if(host&&(p.host===null||hash(p.risk)!==hash(risk)||p.observation!==null&&(!exact(p.observation,['contract','nativeNonce','instances','idle','reasons','observedAt','ingressFrozen','imInFlightVerified'])||p.observation.contract!==1||!uuid(p.observation.nativeNonce)||!exact(p.observation.instances,['onboarding','agents','gateway','im'])||Object.values(p.observation.instances).some(x=>x!==null&&!uuid(x))||typeof p.observation.idle!=='boolean'||!Array.isArray(p.observation.reasons)||p.observation.reasons.some(x=>!['component_unavailable','own_work_active','native_work_active_or_unknown','im_maintenance_pending'].includes(x))||!Number.isSafeInteger(p.observation.observedAt)||p.observation.ingressFrozen!==false||p.observation.imInFlightVerified!==false)))fail('RUNTIME_STOP_PLAN_REVIEW');
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
async function runtimeStopSnapshot(j){
|
|
33
|
+
const members={worker:null,child:null,launcher:null};for(const [key,m] of [['worker',j.worker],['child',j.child],['launcher',j.restart?.launcher]])if(await runtimeAlive(m))members[key]=m;
|
|
34
|
+
return {members,host:await runtimeAlive(j.host)?j.host:null};
|
|
35
|
+
}
|
|
36
|
+
async function runtimeStopView(q,j){
|
|
37
|
+
const d=await runtimeStopLedger(q,j),row=d.records.at(-1);if(!row)return null;
|
|
38
|
+
const p=runtimeStopPlan(q,j,row.plan);let active=0;for(const m of Object.values(p.members))if(await runtimeAlive(m))active++;
|
|
39
|
+
return {id:p.id,phase:row.phase,scope:p.scope,activeInstallerMembers:active,reconcileRequired:row.phase!=='ended',stopPlan:row.plan,error:row.error,hostActive:p.schema===2&&await runtimeAlive(p.host),hostStopAuthorized:p.schema===2,nativeInputMayAbort:p.schema===2,automaticRetry:false};
|
|
40
|
+
}
|
|
41
|
+
async function runtimeStopPreview(q,j){
|
|
42
|
+
if(process.platform!=='linux')fail('RUNTIME_PLATFORM_UNSUPPORTED');
|
|
43
|
+
const d=await runtimeStopLedger(q,j),s=await runtimeStopSnapshot(j),now=Date.now();
|
|
44
|
+
let observation=null;if(s.host&&typeof observeHost==='function')try{observation=await observeHost(q,j);}catch{}
|
|
45
|
+
const active=Object.values(s.members).some(Boolean),reason=d.records.some(x=>x.phase!=='ended')?'prior_stop_unresolved':s.host?(active?'installer_active':!observation?'host_observation_required':!observation.idle?'host_busy':'ready'):active?'ready':'no_active_installer';
|
|
46
|
+
const host=!!s.host,p={schema:host?2:1,id:randomUUID(),operationId:q.operationId,runtimeId:j.id,revision:j.revision,scope:host?hostScope:'installer_processes_only',createdAt:now,expiresAt:now+300000,...s,canApply:reason==='ready',reason,...(host?{risk,observation}:{})};
|
|
47
|
+
return {operationId:q.operationId,runtimeId:j.id,initialization:false,hostStarted:host,scope:p.scope,canApply:p.canApply,reason,expiresAt:p.expiresAt,stopPlan:signed(p,q.proof),hostStopAuthorized:false,...(host?{risk,observation}:{})};
|
|
48
|
+
}
|
|
49
|
+
async function runtimeStop(q,j,io={}){
|
|
50
|
+
if(process.platform!=='linux')fail('RUNTIME_PLATFORM_UNSUPPORTED');
|
|
51
|
+
const p=runtimeStopPlan(q,j,q.stopPlan),d=await runtimeStopLedger(q,j),old=d.records.find(x=>x.plan.id===p.id);
|
|
52
|
+
if(old){if(hash(old.plan)!==hash(q.stopPlan))fail('RUNTIME_STOP_PLAN_REVIEW');
|
|
53
|
+
if(q.action==='runtime_stop_reconcile'){
|
|
54
|
+
for(const m of [...Object.values(p.members),...(p.schema===2?[p.host]:[])])if(await runtimeAlive(m))fail('RUNTIME_STOP_STILL_ACTIVE');
|
|
55
|
+
if(old.phase!=='ended'){old.phase='ended';old.updatedAt=Date.now();await atomicJSON(stopPath(q),signed(d,q.proof));}
|
|
56
|
+
}
|
|
57
|
+
return {...await runtimeView(q,j),stopRequested:true,notResent:true};
|
|
58
|
+
}
|
|
59
|
+
if(q.action==='runtime_stop_reconcile')fail('RUNTIME_STOP_NOT_REQUESTED');
|
|
60
|
+
const host=p.schema===2;if(host&&q.acknowledgeNativeInterruption!==true)fail('RUNTIME_HOST_STOP_CONSENT_REQUIRED');
|
|
61
|
+
if(!p.canApply||p.reason!=='ready')fail(p.reason==='host_drain_required'?'RUNTIME_HOST_DRAIN_REQUIRED':'RUNTIME_STOP_NOT_READY');
|
|
62
|
+
if(p.expiresAt<=Date.now()||p.createdAt>Date.now()+1000)fail('RUNTIME_STOP_PLAN_EXPIRED');
|
|
63
|
+
if(d.records.some(x=>x.phase!=='ended'))fail('RUNTIME_STOP_RECONCILE_REQUIRED');if(d.records.length>=32)fail('RUNTIME_STOP_HISTORY_LIMIT');
|
|
64
|
+
const fresh=await runtimeRecord(q),s=await runtimeStopSnapshot(fresh);
|
|
65
|
+
if(!host&&s.host)fail('RUNTIME_HOST_DRAIN_REQUIRED');if(fresh.revision!==p.revision||hash(s.members)!==hash(p.members)||host&&hash(s.host)!==hash(p.host))fail('RUNTIME_STOP_PLAN_DRIFT');
|
|
66
|
+
const checkHost=async current=>{const o=await observeHost(q,current);if(!o.idle)fail('RUNTIME_HOST_BUSY');if(hash(o.instances)!==hash(p.observation?.instances)||o.nativeNonce!==p.observation?.nativeNonce)fail('RUNTIME_STOP_PLAN_DRIFT');};
|
|
67
|
+
if(host)try{await checkHost(fresh);}catch{fail('RUNTIME_HOST_STOP_PREFLIGHT_REFUSED');}
|
|
68
|
+
const row={plan:q.stopPlan,phase:'requested',updatedAt:Date.now(),error:null};d.records.push(row);await atomicJSON(stopPath(q),signed(d,q.proof));
|
|
69
|
+
try{
|
|
70
|
+
await io.afterStopIntent?.();
|
|
71
|
+
const latest=await runtimeRecord(q);if(!host&&await runtimeAlive(latest.host))fail('RUNTIME_HOST_DRAIN_REQUIRED');
|
|
72
|
+
if(latest.id!==j.id||latest.revision!==p.revision)fail('RUNTIME_STOP_PLAN_DRIFT');
|
|
73
|
+
if(host){
|
|
74
|
+
if(hash(latest.host)!==hash(p.host)||Object.values((await runtimeStopSnapshot(latest)).members).some(Boolean))fail('RUNTIME_STOP_PLAN_DRIFT');
|
|
75
|
+
await checkHost(latest);if(!await runtimeAlive(p.host))fail('RUNTIME_STOP_PLAN_DRIFT');
|
|
76
|
+
process.kill(p.host.pid,'SIGTERM');
|
|
77
|
+
}else{
|
|
78
|
+
// A native Host is detached into another process group. Only recorded installer
|
|
79
|
+
// members are signalled; birth identities are checked immediately before use.
|
|
80
|
+
if(await runtimeAlive(p.members.launcher))process.kill(p.members.launcher.pid,'SIGTERM');
|
|
81
|
+
const member=await runtimeAlive(p.members.worker)?p.members.worker:await runtimeAlive(p.members.child)?p.members.child:null;
|
|
82
|
+
if(member){const raw=await fs.readFile('/proc/'+member.pid+'/stat','utf8'),parts=raw.slice(raw.lastIndexOf(')')+2).split(' ');if(Number(parts[2])!==j.worker?.pid)fail('RUNTIME_PROCESS_REVIEW');process.kill(-j.worker.pid,'SIGTERM');}
|
|
83
|
+
}
|
|
84
|
+
row.phase='signalled';row.updatedAt=Date.now();await atomicJSON(stopPath(q),signed(d,q.proof));
|
|
85
|
+
}catch(e){row.phase='needs_review';row.error=/^ONBOARDING_CLOUD_[A-Z_]+$/.test(e.message)?e.message:'ONBOARDING_CLOUD_RUNTIME_STOP_UNKNOWN';row.updatedAt=Date.now();await atomicJSON(stopPath(q),signed(d,q.proof));throw e;}
|
|
86
|
+
return {...await runtimeView(q,await runtimeRecord(q)),stopRequested:true};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {ledger:runtimeStopLedger,view:runtimeStopView,preview:runtimeStopPreview,apply:runtimeStop};
|
|
90
|
+
}
|