workshop-dedup-acceptance.mjs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. // Standalone sample acceptance. Uses the existing secure Skill entry; no identity creation.
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import {fileURLToPath} from 'node:url';
  5. import {spawnSync} from 'node:child_process';
  6. export const TASK_KEY='KSF-查重实测-20261102-01';
  7. const fail=code=>{throw Object.assign(Error(code),{code});};
  8. const check=(ok,code='DEDUP_ACCEPTANCE_MISMATCH')=>{if(!ok)fail(code);};
  9. const safeCode=e=>/^[A-Z][A-Z0-9_]+$/.test(e?.code||'')?e.code:'DEDUP_ACCEPTANCE_FAILED';
  10. export function validateDedupContext(c){
  11. check(c&&typeof c==='object'&&!Array.isArray(c),'INVALID_DEDUP_CONTEXT');
  12. check(Object.keys(c).length===3&&Object.keys(c).every(k=>['version','taskKey','agentProfileKey'].includes(k)),'INVALID_DEDUP_CONTEXT');
  13. check(c.version==='ksf.dedup-acceptance.v1'&&c.taskKey===TASK_KEY,'INVALID_DEDUP_CONTEXT');
  14. check(typeof c.agentProfileKey==='string'&&/^agent-account:[a-f0-9]{64}$/.test(c.agentProfileKey),'TRUSTED_AGENT_PROFILE_REQUIRED');
  15. return c;
  16. }
  17. export function dedupTask(round){
  18. check(round==='a'||round==='b','INVALID_DEDUP_ROUND');
  19. return {
  20. source:{title:'【样例】原料查重验收 '+TASK_KEY,sourceKind:'dataset',dataMode:'sample',rawText:TASK_KEY+'。本段为人工构造的查重样例,不来自真实平台或供应商。原料:演练面粉占位;类型:配料;规格、供应商、价格和用量均未知。只用于验证相同资料跨批次复用,不构成配方或采购建议。'},
  21. records:[{itemKey:'material',className:'KfMaterial',externalKey:TASK_KEY+':material:'+round,data:{dataMode:'sample',name:'【样例】查重演练面粉 '+TASK_KEY,materialType:'ingredient',specification:'未知;无供应商或检测依据',function:'仅验证相同原料跨批次复用;不用于真实配方'}}]
  22. };
  23. }
  24. function preflight(p){
  25. check(p?.readOnly===true&&p.schema==='ksf.ingest-dedup.v1'&&p.items?.length===1&&p.items[0].itemKey==='material','DEDUP_PREFLIGHT_MISMATCH');
  26. return p.items[0];
  27. }
  28. function imported(r){
  29. check(r?.status==='succeeded'&&r.dataMode==='sample'&&r.formalReportPublished===false&&r.deduplication?.schema==='ksf.ingest-dedup.v1'&&r.deduplication.checked===1,'DEDUP_IMPORT_MISMATCH');
  30. check(typeof r.sourceId==='string'&&r.sourceId&&r.items?.length===1&&r.items[0].itemKey==='material'&&r.items[0].className==='KfMaterial'&&r.items[0].status==='succeeded'&&typeof r.items[0].objectId==='string'&&r.items[0].objectId,'DEDUP_IMPORT_MISMATCH');
  31. return {sourceId:r.sourceId,materialId:r.items[0].objectId,jobId:r.items[0].jobId,sourceOperation:r.sourceOperation,operation:r.items[0].operation,created:r.counts?.created,unchanged:r.counts?.unchanged};
  32. }
  33. export async function runDedupAcceptance({context,invoke,checkpoint=()=>{}}){
  34. validateDedupContext(context);
  35. const rounds={},run=async(command,task,suffix)=>{
  36. const requestId=TASK_KEY+':'+suffix;
  37. checkpoint({status:'in-flight',command,requestId,rounds:structuredClone(rounds)});
  38. const result=await invoke(command,task,requestId);
  39. checkpoint({status:'received',command,requestId,result,rounds:structuredClone(rounds)});
  40. return result;
  41. };
  42. const contract=await run('workshop-contract',{},'contract');
  43. check(contract?.deduplication?.schema==='ksf.ingest-dedup.v1'&&contract.deduplication.preflightFunction==='ksfCheckDuplicates','DEDUP_CAPABILITY_UNAVAILABLE');
  44. const a=dedupTask('a'),b=dedupTask('b');
  45. const beforeA=await run('workshop-dedup',a,'preflight-a');
  46. const first=preflight(beforeA);
  47. check(beforeA.source?.operation==='no-exact-match'&&['deferred','no-exact-match'].includes(first.decision),'DEDUP_HISTORY_EXISTS');
  48. rounds.a=imported(await run('workshop-import',a,'batch-a'));
  49. checkpoint({status:'round-a-recorded',rounds:structuredClone(rounds)});
  50. check(rounds.a.sourceOperation==='created'&&rounds.a.operation==='created'&&rounds.a.created===1&&rounds.a.unchanged===0,'DEDUP_FIRST_IMPORT_MISMATCH');
  51. const beforeB=await run('workshop-dedup',b,'preflight-b');
  52. const second=preflight(beforeB);
  53. check(beforeB.source?.operation==='unchanged'&&beforeB.source.sourceId===rounds.a.sourceId&&second.decision==='reuse'&&second.objectId===rounds.a.materialId,'DEDUP_REUSE_PREFLIGHT_MISMATCH');
  54. rounds.b=imported(await run('workshop-import',b,'batch-b'));
  55. checkpoint({status:'round-b-recorded',rounds:structuredClone(rounds)});
  56. check(rounds.b.sourceId===rounds.a.sourceId&&rounds.b.materialId===rounds.a.materialId,'DEDUP_ID_MISMATCH');
  57. check(rounds.b.sourceOperation==='unchanged'&&rounds.b.operation==='unchanged'&&rounds.b.created===0&&rounds.b.unchanged===1,'DEDUP_SECOND_IMPORT_MISMATCH');
  58. check(rounds.a.jobId&&rounds.b.jobId&&rounds.a.jobId!==rounds.b.jobId,'DEDUP_BATCH_JOB_MISMATCH');
  59. const {record}=await run('workshop-read',{className:'KfMaterial',objectId:rounds.a.materialId},'read-material');
  60. check(record?.objectId===rounds.a.materialId&&record.dataMode==='sample'&&record.sourceRef?.objectId===rounds.a.sourceId,'DEDUP_READBACK_MISMATCH');
  61. for(const [k,v]of Object.entries(a.records[0].data))check(record[k]===v,'DEDUP_READBACK_MISMATCH');
  62. const receipt={ok:true,operation:'sample-material-dedup-verified',taskKey:TASK_KEY,dataMode:'sample',rounds,sameSourceId:true,sameMaterialId:true,secondCreated:0,secondUnchanged:1,secondReused:1,reusedMeaning:'unchanged',materialReadbackVerified:true,formalReportPublished:false,actualPlatformCollectionPerformed:false,auditRecordsRetained:true};
  63. checkpoint({status:'complete',receipt});return receipt;
  64. }
  65. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
  66. try{
  67. const a=process.argv.slice(2);
  68. check(a.length===5&&a[0]==='--input'&&a[2]==='--state-dir'&&a[4]==='--execute','USAGE');
  69. check(process.platform==='linux'&&process.geteuid()===10000,'HERMES_UID_REQUIRED');
  70. check(path.isAbsolute(a[1])&&path.isAbsolute(a[3]),'ABSOLUTE_PATH_REQUIRED');
  71. const parent=path.dirname(a[3]);
  72. for(const file of [a[1],parent])check(fs.realpathSync(file)===file,'UNSAFE_DEDUP_PATH');
  73. const st=fs.lstatSync(a[1]),pt=fs.lstatSync(parent);
  74. check(st.isFile()&&st.uid===process.geteuid()&&(st.mode&0o777)===0o600,'PRIVATE_CONTEXT_REQUIRED');
  75. check(pt.isDirectory()&&pt.uid===process.geteuid()&&(pt.mode&0o777)===0o700,'PRIVATE_PARENT_REQUIRED');
  76. const context=validateDedupContext(JSON.parse(fs.readFileSync(a[1],'utf8')));
  77. const worker='/opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs';
  78. check(fs.statSync(worker).isFile(),'SECURE_ENTRY_UNAVAILABLE');
  79. check(!fs.existsSync(a[3]),'DEDUP_STATE_EXISTS');
  80. fs.mkdirSync(a[3],{mode:0o700});
  81. check(fs.realpathSync(a[3])===a[3],'UNSAFE_DEDUP_PATH');
  82. const journal={version:'ksf.dedup-journal.v1',taskKey:TASK_KEY,at:new Date().toISOString(),status:'running',steps:[]};
  83. const persist=()=>fs.writeFileSync(path.join(a[3],'journal.json'),JSON.stringify(journal,null,2),{mode:0o600});persist();
  84. let sequence=0;const invocations=[];
  85. const invoke=async(command,task,requestId)=>{
  86. check(['workshop-contract','workshop-dedup','workshop-import','workshop-read'].includes(command),'COMMAND_NOT_ALLOWED');
  87. const name=String(++sequence).padStart(2,'0')+'-'+command,input=path.join(a[3],name+'.input.json');
  88. fs.writeFileSync(input,JSON.stringify(task,null,2),{flag:'wx',mode:0o600});
  89. const args=[worker,command,...(command==='workshop-import'?['--agent-profile',context.agentProfileKey]:[]),'--input',input,'--request-id',requestId];
  90. // No retry: timeout/transport errors can occur after a remote write has committed.
  91. const p=spawnSync(process.execPath,args,{encoding:'utf8',timeout:660000,maxBuffer:4*1024*1024,windowsHide:true});
  92. fs.writeFileSync(path.join(a[3],name+'.transport.json'),JSON.stringify({exitCode:p.status,signal:p.signal,stdout:p.stdout||'',stderr:p.stderr||'',spawnFailed:!!p.error}),{flag:'wx',mode:0o600});
  93. if(p.error||p.signal)fail('DEDUP_REQUEST_UNCERTAIN');
  94. // executeSecure writes a valid ok=false child envelope to stdout with exit 1;
  95. // wrapper-level failures use stderr. Preserve either envelope before stopping.
  96. let receipt;try{receipt=JSON.parse(p.stdout?.trim()?p.stdout:p.stderr);}catch{fail('DEDUP_RECEIPT_INVALID');}
  97. fs.writeFileSync(path.join(a[3],name+'.receipt.json'),JSON.stringify(receipt,null,2),{flag:'wx',mode:0o600});
  98. if(!(p.status===0&&receipt?.ok===true&&receipt.entryResult?.ok===true)){
  99. const code=receipt?.entryResult?.error?.code||receipt?.error?.code;
  100. fail(/^[A-Z][A-Z0-9_]+$/.test(code||'')?code:'DEDUP_COMMAND_FAILED');
  101. }
  102. check(receipt.command===command&&receipt.requestId===requestId&&receipt.entryResult.command===command&&receipt.entryResult.requestId===requestId,'DEDUP_RECEIPT_BINDING_MISMATCH');
  103. invocations.push({command,requestId,invocationId:receipt.invocationId,entryInvocationId:receipt.entryResult.invocationId});
  104. return receipt.entryResult.result;
  105. };
  106. try{
  107. const receipt=await runDedupAcceptance({context,invoke,checkpoint:event=>{journal.steps.push(event);journal.status=event.status;persist();}});
  108. receipt.importInvocations=invocations.filter(x=>x.command==='workshop-import');
  109. fs.writeFileSync(path.join(a[3],'summary.json'),JSON.stringify(receipt,null,2),{flag:'wx',mode:0o600});
  110. console.log(JSON.stringify(receipt,null,2));
  111. }catch(e){journal.status='stopped';journal.error={code:safeCode(e),message:'保留原键、已返回编号和全部回执;不重试、不删目录、不换键。'};persist();throw e;}
  112. }catch(e){console.error(JSON.stringify({ok:false,error:{code:safeCode(e),message:'查重验收停止;保留现场与回执,不输出账号或凭据。'}}));process.exitCode=1;}
  113. }