@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.
Files changed (271) hide show
  1. package/LICENSE +58 -0
  2. package/LICENSE.en.md +94 -0
  3. package/README.md +88 -2
  4. package/bundle/ASSET_NOTICE.md +19 -0
  5. package/bundle/LICENSE +58 -0
  6. package/bundle/LICENSE.en.md +94 -0
  7. package/bundle/README.md +75 -0
  8. package/bundle/THIRD_PARTY_NOTICES.md +94 -0
  9. package/bundle/a2a-dispatches.js +38 -0
  10. package/bundle/a2a-endpoint.js +112 -0
  11. package/bundle/a2a-tools.js +11 -0
  12. package/bundle/a2a-worker.js +63 -0
  13. package/bundle/approval-auth.js +5 -0
  14. package/bundle/approval-chat.js +150 -0
  15. package/bundle/approval-feishu.js +52 -0
  16. package/bundle/approval-guidance.js +68 -0
  17. package/bundle/approval-http.js +8 -0
  18. package/bundle/approval-ledger.js +142 -0
  19. package/bundle/approval-local-relay.js +86 -0
  20. package/bundle/approval-receipts.js +62 -0
  21. package/bundle/approval-reference.js +33 -0
  22. package/bundle/approval-semantic-model.js +26 -0
  23. package/bundle/approval-semantics.js +51 -0
  24. package/bundle/approval-ui/app.js +1 -0
  25. package/bundle/approval-ui/index.html +1 -0
  26. package/bundle/approval-ui/style.css +1 -0
  27. package/bundle/approval-workbench.js +45 -0
  28. package/bundle/artifact-media.js +34 -0
  29. package/bundle/bounded-text.js +44 -0
  30. package/bundle/compatibility.json +980 -0
  31. package/bundle/cordis.patch.yml +5 -0
  32. package/bundle/delivery-approval.js +133 -0
  33. package/bundle/delivery-artifacts.js +52 -0
  34. package/bundle/delivery-commit.js +125 -0
  35. package/bundle/delivery-contract.js +42 -0
  36. package/bundle/delivery-git-read.js +217 -0
  37. package/bundle/delivery-index.js +23 -0
  38. package/bundle/delivery-journal.js +207 -0
  39. package/bundle/delivery-object-plan.js +138 -0
  40. package/bundle/delivery-preview.js +153 -0
  41. package/bundle/delivery-process-receipts.js +90 -0
  42. package/bundle/delivery-process.js +46 -0
  43. package/bundle/delivery-process.py +228 -0
  44. package/bundle/delivery-push-approval.js +89 -0
  45. package/bundle/delivery-push-plan.js +98 -0
  46. package/bundle/delivery-push-recovery.js +32 -0
  47. package/bundle/delivery-push.js +83 -0
  48. package/bundle/delivery-recovery.js +143 -0
  49. package/bundle/delivery-runtime.js +182 -0
  50. package/bundle/delivery-ssh.js +64 -0
  51. package/bundle/delivery-state.js +106 -0
  52. package/bundle/development-attention.js +35 -0
  53. package/bundle/development-bridge.js +136 -0
  54. package/bundle/development-command-review.js +110 -0
  55. package/bundle/development-container.js +110 -0
  56. package/bundle/development-contract.js +17 -0
  57. package/bundle/development-files.js +101 -0
  58. package/bundle/development-gate.js +236 -0
  59. package/bundle/development-model-budget.js +48 -0
  60. package/bundle/development-peer.js +155 -0
  61. package/bundle/development-plan.js +43 -0
  62. package/bundle/development-reconciliation.js +62 -0
  63. package/bundle/development-review.js +81 -0
  64. package/bundle/development-rules/AGENTS.md +51 -0
  65. package/bundle/development-runtime.js +68 -0
  66. package/bundle/development-source.js +48 -0
  67. package/bundle/development-test-runner.js +13 -0
  68. package/bundle/development-tools.js +45 -0
  69. package/bundle/development-worker-review.js +110 -0
  70. package/bundle/development-worker.js +68 -0
  71. package/bundle/development-workspace.js +54 -0
  72. package/bundle/diagnostics.js +78 -0
  73. package/bundle/distribution-lock.json +773 -0
  74. package/bundle/drained-process.js +26 -0
  75. package/bundle/durable-inbox.js +294 -0
  76. package/bundle/feishu-policy.js +24 -0
  77. package/bundle/gateway-maintenance.js +55 -0
  78. package/bundle/global-rules/AGENTS.md +83 -0
  79. package/bundle/im-delivery.js +60 -0
  80. package/bundle/index.js +445 -0
  81. package/bundle/intervention-actions.js +135 -0
  82. package/bundle/intervention-job-control.js +28 -0
  83. package/bundle/intervention-state.js +43 -0
  84. package/bundle/job-version.js +15 -0
  85. package/bundle/legacy-feishu-adapter.js +89 -0
  86. package/bundle/link-contract.js +80 -0
  87. package/bundle/maintenance-process-identity.js +93 -0
  88. package/bundle/model-selection.js +71 -0
  89. package/bundle/native-context.js +14 -0
  90. package/bundle/native-entry-admission.js +2 -0
  91. package/bundle/native-entry.js +58 -0
  92. package/bundle/native-file-reference.js +32 -0
  93. package/bundle/native-owner-origin.js +12 -0
  94. package/bundle/native-rules/AGENTS.md +199 -0
  95. package/bundle/native-tools.js +108 -0
  96. package/bundle/notification-artifacts.js +70 -0
  97. package/bundle/notification-contract.js +29 -0
  98. package/bundle/notification-defaults.js +81 -0
  99. package/bundle/notification-discovery.js +65 -0
  100. package/bundle/notification-endpoint.js +54 -0
  101. package/bundle/notification-feishu.js +51 -0
  102. package/bundle/notification-media-endpoint.js +94 -0
  103. package/bundle/notification-review.js +27 -0
  104. package/bundle/notification-targets.js +54 -0
  105. package/bundle/onboarding-activity.js +66 -0
  106. package/bundle/onboarding-agent-hold.js +94 -0
  107. package/bundle/onboarding-boundary.js +20 -0
  108. package/bundle/onboarding-cloud-contract.js +29 -0
  109. package/bundle/onboarding-cloud-file-review.js +58 -0
  110. package/bundle/onboarding-cloud-files.js +100 -0
  111. package/bundle/onboarding-cloud-im.js +34 -0
  112. package/bundle/onboarding-cloud-model-probe.js +53 -0
  113. package/bundle/onboarding-cloud-models.js +68 -0
  114. package/bundle/onboarding-cloud-operation.js +276 -0
  115. package/bundle/onboarding-cloud-package.js +38 -0
  116. package/bundle/onboarding-cloud-repositories.js +89 -0
  117. package/bundle/onboarding-cloud-retirement.js +59 -0
  118. package/bundle/onboarding-cloud-task-runtime.js +90 -0
  119. package/bundle/onboarding-cloud-web.js +85 -0
  120. package/bundle/onboarding-cloud.js +110 -0
  121. package/bundle/onboarding-coordinated-hold.js +94 -0
  122. package/bundle/onboarding-exclusive-rename.js +16 -0
  123. package/bundle/onboarding-existing-host.js +63 -0
  124. package/bundle/onboarding-existing-setup.js +123 -0
  125. package/bundle/onboarding-file-contract.js +31 -0
  126. package/bundle/onboarding-file-review-contract.js +38 -0
  127. package/bundle/onboarding-file-transfer.js +133 -0
  128. package/bundle/onboarding-handoff-remote.js +98 -0
  129. package/bundle/onboarding-handoff.js +112 -0
  130. package/bundle/onboarding-host-stop-observation.js +44 -0
  131. package/bundle/onboarding-host.js +254 -0
  132. package/bundle/onboarding-identity.js +9 -0
  133. package/bundle/onboarding-im-maintenance-native.js +30 -0
  134. package/bundle/onboarding-im-maintenance.js +70 -0
  135. package/bundle/onboarding-im.js +31 -0
  136. package/bundle/onboarding-inspection-guard.js +15 -0
  137. package/bundle/onboarding-install-fs.js +58 -0
  138. package/bundle/onboarding-installer.js +157 -0
  139. package/bundle/onboarding-local-pair-native.js +21 -0
  140. package/bundle/onboarding-local-pair.js +99 -0
  141. package/bundle/onboarding-maintenance-fs.js +69 -0
  142. package/bundle/onboarding-maintenance-plan.js +213 -0
  143. package/bundle/onboarding-maintenance-record.js +169 -0
  144. package/bundle/onboarding-maintenance.js +163 -0
  145. package/bundle/onboarding-model-probe.js +119 -0
  146. package/bundle/onboarding-models.js +107 -0
  147. package/bundle/onboarding-native-host.js +101 -0
  148. package/bundle/onboarding-native-probe.js +56 -0
  149. package/bundle/onboarding-native-runner.js +29 -0
  150. package/bundle/onboarding-node-bootstrap.py +678 -0
  151. package/bundle/onboarding-node.js +77 -0
  152. package/bundle/onboarding-notification-defaults.js +93 -0
  153. package/bundle/onboarding-package-source.js +33 -0
  154. package/bundle/onboarding-paths.js +58 -0
  155. package/bundle/onboarding-peer-binding.js +61 -0
  156. package/bundle/onboarding-peer-contract.js +12 -0
  157. package/bundle/onboarding-plugin-repair.js +116 -0
  158. package/bundle/onboarding-plugin-version.js +143 -0
  159. package/bundle/onboarding-remote-control.js +140 -0
  160. package/bundle/onboarding-remote-probe.js +40 -0
  161. package/bundle/onboarding-repositories.js +88 -0
  162. package/bundle/onboarding-repository.js +49 -0
  163. package/bundle/onboarding-runtime-activation.js +83 -0
  164. package/bundle/onboarding-runtime-artifacts-client.js +87 -0
  165. package/bundle/onboarding-runtime-artifacts.js +126 -0
  166. package/bundle/onboarding-runtime-extension.js +400 -0
  167. package/bundle/onboarding-runtime-inventory.js +88 -0
  168. package/bundle/onboarding-runtime-program.js +42 -0
  169. package/bundle/onboarding-runtime-reclaim-client.js +94 -0
  170. package/bundle/onboarding-runtime-reclaim-worker.js +63 -0
  171. package/bundle/onboarding-runtime-reclaim.js +132 -0
  172. package/bundle/onboarding-runtime-repair-client.js +50 -0
  173. package/bundle/onboarding-runtime-repair.js +88 -0
  174. package/bundle/onboarding-runtime-stop.js +90 -0
  175. package/bundle/onboarding-runtime-uninstall-client.js +88 -0
  176. package/bundle/onboarding-runtime-uninstall.js +167 -0
  177. package/bundle/onboarding-runtime-version-client.js +138 -0
  178. package/bundle/onboarding-runtime-version.js +145 -0
  179. package/bundle/onboarding-schema.js +42 -0
  180. package/bundle/onboarding-self-disable.js +3 -0
  181. package/bundle/onboarding-self-lifecycle.js +95 -0
  182. package/bundle/onboarding-self-maintenance.js +52 -0
  183. package/bundle/onboarding-self-remove.js +3 -0
  184. package/bundle/onboarding-self-stop.js +85 -0
  185. package/bundle/onboarding-ssh-forward.js +48 -0
  186. package/bundle/onboarding-ssh.js +105 -0
  187. package/bundle/onboarding-state.js +95 -0
  188. package/bundle/onboarding-task-runtime-contract.js +38 -0
  189. package/bundle/onboarding-task-runtime-native.js +80 -0
  190. package/bundle/onboarding-task-runtime.js +66 -0
  191. package/bundle/onboarding-token-plan.js +52 -0
  192. package/bundle/onboarding-version-activation-client.js +48 -0
  193. package/bundle/onboarding-version-candidate.js +96 -0
  194. package/bundle/onboarding-version-repair.js +128 -0
  195. package/bundle/package-lock.json +773 -0
  196. package/bundle/package.json +99 -0
  197. package/bundle/peer-file-fs.js +62 -0
  198. package/bundle/peer-file-migration.js +59 -0
  199. package/bundle/peer-file-review.js +107 -0
  200. package/bundle/peer-files.js +157 -0
  201. package/bundle/peer-mailbox.js +75 -0
  202. package/bundle/protocol.js +17 -0
  203. package/bundle/recovery.js +40 -0
  204. package/bundle/repository-catalog.js +85 -0
  205. package/bundle/repository-gitee.js +76 -0
  206. package/bundle/repository-managed.js +46 -0
  207. package/bundle/repository-writer.js +43 -0
  208. package/bundle/rules/AGENTS.md +22 -0
  209. package/bundle/runtime-im-lock.json +11577 -0
  210. package/bundle/runtime-im-package.json +18 -0
  211. package/bundle/runtime-lock.json +10152 -0
  212. package/bundle/runtime-package.json +17 -0
  213. package/bundle/scheduler-migration.js +45 -0
  214. package/bundle/search-contract.js +19 -0
  215. package/bundle/skills/dsh-dual-end/SKILL.md +12 -0
  216. package/bundle/skills/task-a2a/SKILL.md +55 -0
  217. package/bundle/skills/task-a2a-worker/SKILL.md +23 -0
  218. package/bundle/skills/task-approval/SKILL.md +128 -0
  219. package/bundle/skills/task-artifacts/SKILL.md +46 -0
  220. package/bundle/skills/task-development/SKILL.md +144 -0
  221. package/bundle/skills/task-diagnostics/SKILL.md +41 -0
  222. package/bundle/skills/task-direct-link/SKILL.md +143 -0
  223. package/bundle/skills/task-file-transfer/SKILL.md +107 -0
  224. package/bundle/skills/task-handoff/SKILL.md +23 -0
  225. package/bundle/skills/task-intake/SKILL.md +13 -0
  226. package/bundle/skills/task-notify/SKILL.md +102 -0
  227. package/bundle/skills/task-onboarding/SKILL.md +708 -0
  228. package/bundle/skills/task-recovery/SKILL.md +54 -0
  229. package/bundle/skills/task-repository/SKILL.md +145 -0
  230. package/bundle/skills/task-research/SKILL.md +43 -0
  231. package/bundle/skills/task-schedule/SKILL.md +79 -0
  232. package/bundle/skills/task-sidebar/SKILL.md +122 -0
  233. package/bundle/skills/task-workbench/SKILL.md +30 -0
  234. package/bundle/skills/task-worker/SKILL.md +88 -0
  235. package/bundle/source-search.js +89 -0
  236. package/bundle/source-web.js +17 -0
  237. package/bundle/ssh-connector.js +24 -0
  238. package/bundle/store.js +102 -0
  239. package/bundle/task-a2a.js +298 -0
  240. package/bundle/task-artifacts.js +179 -0
  241. package/bundle/task-board-a2a.js +55 -0
  242. package/bundle/task-board-development.js +83 -0
  243. package/bundle/task-board-planning.js +84 -0
  244. package/bundle/task-board.js +132 -0
  245. package/bundle/task-branch.js +2 -0
  246. package/bundle/task-catalog.js +77 -0
  247. package/bundle/task-context.js +40 -0
  248. package/bundle/task-envelope.js +96 -0
  249. package/bundle/task-interventions.js +54 -0
  250. package/bundle/task-jobs.js +156 -0
  251. package/bundle/task-notifications.js +132 -0
  252. package/bundle/task-results.js +16 -0
  253. package/bundle/task-scheduler.js +77 -0
  254. package/bundle/task-sources.js +101 -0
  255. package/bundle/task-trigger-time.js +46 -0
  256. package/bundle/task-triggers.js +157 -0
  257. package/bundle/task-worker.js +73 -0
  258. package/bundle/task-workflows.js +117 -0
  259. package/bundle/tools.js +13 -0
  260. package/bundle/workbench-assets/asset-provenance.json +26 -0
  261. package/bundle/workbench-assets/whale-ui-states.png +0 -0
  262. package/bundle/workbench-client.js +811 -0
  263. package/bundle/workbench-decisions.js +48 -0
  264. package/bundle/workbench-host.js +122 -0
  265. package/bundle/workbench-im-identity.js +51 -0
  266. package/bundle/workbench-proxy.js +127 -0
  267. package/bundle/worker-rules/AGENTS.md +84 -0
  268. package/bundle/worker-tools.js +13 -0
  269. package/compatibility.json +980 -0
  270. package/cordis.patch.yml +3 -0
  271. package/package.json +57 -4
@@ -0,0 +1,90 @@
1
+ // Private durable supervisor evidence. Not a signature or OS identity boundary:
2
+ // an administrator able to rewrite the journal and state directory remains trusted.
3
+ import {mkdirSync,lstatSync,realpathSync,openSync,closeSync,fstatSync,readSync,readFileSync,constants} from 'node:fs';
4
+ import {join} from 'node:path';
5
+ import {randomBytes} from 'node:crypto';
6
+ import {canonical,digest} from './approval-ledger.js';
7
+ import {validateProcessReport} from './delivery-process.js';
8
+ const fail=c=>{throw Error('DELIVERY_RECEIPT_'+c);};
9
+ const same=(a,b)=>canonical(a)===canonical(b);
10
+ const stamp=s=>[s.dev,s.ino,s.mode,s.uid,s.nlink,s.size,s.mtimeNs,s.ctimeNs].map(String).join(':');
11
+ const sha=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x);
12
+ const uuid=x=>typeof x==='string'&&/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(x);
13
+ export function bootId(){const id=readFileSync('/proc/sys/kernel/random/boot_id','ascii').trim();if(!uuid(id))fail('BOOT_UNKNOWN');return id;}
14
+ export function supervisorLive(pid,start,boot){
15
+ if(bootId()!==boot)return false;
16
+ let raw;try{raw=readFileSync('/proc/'+pid+'/stat','ascii');}catch(e){if(e.code==='ENOENT')return false;throw e;}
17
+ const rest=raw.slice(raw.lastIndexOf(')')+2).split(' ');
18
+ if(rest.length<20||!/^\d+$/.test(rest[19])||!rest[0])fail('SUPERVISOR_UNKNOWN');
19
+ return rest[19]===start&&!['Z','X'].includes(rest[0]);
20
+ }
21
+ export function validateReceiptBinding(v){
22
+ if(!v||Object.keys(v).sort().join(',')!=='action_id,binding_hash,boot_id,directory,epoch,intent_fingerprint,nonce,operation_id,plan_fingerprint,schema'||v.schema!==1||
23
+ !/^delivery-op-[a-f0-9-]{36}$/.test(v.operation_id??'')||!/^delivery-[a-f0-9-]{36}$/.test(v.action_id??'')||
24
+ !Number.isSafeInteger(v.epoch)||v.epoch<1||![v.binding_hash,v.nonce,v.intent_fingerprint,v.plan_fingerprint].every(sha)||!uuid(v.boot_id)||
25
+ !v.directory||Object.keys(v.directory).sort().join(',')!=='device,inode,path'||typeof v.directory.path!=='string'||!v.directory.path.startsWith('/')||
26
+ v.directory.path.length>2048||/[\x00-\x1f\x7f]/.test(v.directory.path)||!/^\d{1,30}$/.test(v.directory.device)||!/^\d{1,30}$/.test(v.directory.inode))fail('BINDING_INVALID');
27
+ const {binding_hash,...body}=v;if(digest(body)!==binding_hash)fail('BINDING_INVALID');return v;
28
+ }
29
+ export class DeliveryProcessReceipts {
30
+ constructor({journal,directory,enabled=false},io={}){
31
+ this.j=journal;this.db=journal.db;this.directory=directory;this.enabled=enabled;this.boot=io.bootId??bootId;this.live=io.supervisorLive??supervisorLive;
32
+ if(enabled!==true)fail('DISABLED');
33
+ if(typeof directory!=='string'||!directory.startsWith('/'))fail('DIRECTORY_INVALID');
34
+ try{mkdirSync(directory,{mode:0o700});}catch(e){if(e.code!=='EEXIST')throw e;}this.directoryIdentity=this.checkDirectory();
35
+ this.db.exec(`CREATE TABLE IF NOT EXISTS delivery_process_receipts(operation_id TEXT PRIMARY KEY,nonce TEXT UNIQUE NOT NULL,
36
+ binding TEXT NOT NULL,created INTEGER NOT NULL);`);
37
+ }
38
+ checkDirectory(expected){
39
+ const s=lstatSync(this.directory,{bigint:true});
40
+ if(!s.isDirectory()||s.isSymbolicLink()||realpathSync(this.directory)!==this.directory||(s.mode&0o077n)!==0n||s.uid!==BigInt(process.getuid()))fail('DIRECTORY_CHANGED');
41
+ const v={path:this.directory,device:String(s.dev),inode:String(s.ino)};if(expected&&!same(v,expected))fail('DIRECTORY_CHANGED');return v;
42
+ }
43
+ prepare(operation_id){
44
+ if(this.enabled!==true)fail('DISABLED');
45
+ const o=this.j.operation(operation_id),i=this.j.effectAuthority(o.action_id,o.epoch);
46
+ if(o.kind!=='push'||o.state!=='intent'||o.pid!==null||i.kind!=='push-execution-intent-v1')fail('OPERATION_INVALID');
47
+ this.checkDirectory(this.directoryIdentity);
48
+ const body={schema:1,operation_id,action_id:o.action_id,epoch:o.epoch,intent_fingerprint:i.fingerprint,plan_fingerprint:i.plan.fingerprint,
49
+ nonce:randomBytes(32).toString('hex'),boot_id:this.boot(),directory:this.directoryIdentity};
50
+ const b=validateReceiptBinding({...body,binding_hash:digest(body)});
51
+ this.db.prepare('INSERT INTO delivery_process_receipts VALUES (?,?,?,?)').run(operation_id,b.nonce,canonical(b),this.j.now());return b;
52
+ }
53
+ binding(operation_id){
54
+ if(this.enabled!==true)fail('DISABLED');
55
+ const row=this.db.prepare('SELECT * FROM delivery_process_receipts WHERE operation_id=?').get(operation_id);if(!row)fail('BINDING_REQUIRED');
56
+ const b=validateReceiptBinding(JSON.parse(row.binding)),o=this.j.operation(operation_id),i=this.j.intent(this.j.row(o.action_id));
57
+ if(b.operation_id!==operation_id||row.nonce!==b.nonce||b.action_id!==o.action_id||b.epoch!==o.epoch||o.kind!=='push'||
58
+ b.intent_fingerprint!==i.fingerprint||b.plan_fingerprint!==i.plan.fingerprint||!same(b.directory,this.directoryIdentity))fail('OPERATION_CHANGED');
59
+ this.checkDirectory(b.directory);return {b,o};
60
+ }
61
+ inspect(operation_id){
62
+ const {b,o}=this.binding(operation_id),path=join(this.directory,b.nonce+'.json'),before=lstatSync(path,{bigint:true});
63
+ if(!before.isFile()||before.isSymbolicLink()||before.nlink!==1n||(before.mode&0o077n)!==0n||before.uid!==BigInt(process.getuid())||before.size<1n||before.size>16384n)fail('FILE_INVALID');
64
+ const fd=openSync(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);let data;
65
+ try{
66
+ if(stamp(fstatSync(fd,{bigint:true}))!==stamp(before))fail('FILE_CHANGED');
67
+ const bytes=Buffer.alloc(Number(before.size)+1);let n=0,k;while(n<bytes.length&&(k=readSync(fd,bytes,n,bytes.length-n,null))>0)n+=k;
68
+ if(n!==Number(before.size)||stamp(fstatSync(fd,{bigint:true}))!==stamp(before)||stamp(lstatSync(path,{bigint:true}))!==stamp(before))fail('FILE_CHANGED');data=bytes.subarray(0,n);
69
+ }finally{closeSync(fd);}
70
+ this.checkDirectory(b.directory);let r;try{r=JSON.parse(data.toString('utf8'));}catch{fail('FILE_INVALID');}
71
+ if(!r||Object.keys(r).sort().join(',')!=='binding_hash,boot_id,kind,operation_id,pid,report,schema,start_ticks'||r.schema!==1||r.kind!=='sealed-process-receipt-v1'||
72
+ r.binding_hash!==b.binding_hash||r.boot_id!==b.boot_id||r.operation_id!==operation_id||r.pid!==o.pid||!Number.isSafeInteger(r.pid)||r.pid<1||
73
+ typeof r.start_ticks!=='string'||!/^\d{1,30}$/.test(r.start_ticks)||!r.report||Object.keys(r.report).sort().join(',')!=='cause,code,drained,nonce,reaped,schema,signal,started')fail('REPORT_INVALID');
74
+ // Receipt has no stdout/stderr, argv, environment or credential paths.
75
+ const report=validateProcessReport(JSON.stringify({...r.report,stdout:''}),b.nonce,0,null);
76
+ const live=this.live(r.pid,r.start_ticks,r.boot_id);if(typeof live!=='boolean')fail('SUPERVISOR_UNKNOWN');
77
+ const proof={operation_id,action_id:b.action_id,epoch:b.epoch,binding_hash:b.binding_hash,receipt_sha256:digest(data.toString('utf8')),
78
+ state:live?'supervisor_live':'terminal_verified',code:report.code,signal:report.signal,cause:report.cause,started:report.started,reaped:report.reaped,grants_execution:false};
79
+ return {...proof,fingerprint:digest({...proof,file_stamp:stamp(before)})};
80
+ }
81
+ reconcile(operation_id,expectedFingerprint){
82
+ if(!sha(expectedFingerprint))fail('EVIDENCE_REQUIRED');
83
+ const {o}=this.binding(operation_id),row=this.j.fence(o.action_id,o.epoch,['needs_review']);
84
+ if(this.j.store.task(row.task_id).revision!==this.j.intent(row).task_revision)fail('TASK_REVISION_CHANGED');
85
+ const proof=this.inspect(operation_id);
86
+ if(proof.state!=='terminal_verified'||proof.fingerprint!==expectedFingerprint)fail('EVIDENCE_CHANGED');
87
+ this.j.closed(operation_id,{code:proof.code,signal:proof.signal,evidence:{kind:'sealed-process-receipt-v1',binding_hash:proof.binding_hash,receipt_sha256:proof.receipt_sha256}});
88
+ return {operation_id,state:'closed',task_released:false,retry_authorized:false};
89
+ }
90
+ }
@@ -0,0 +1,46 @@
1
+ // Private Linux supervisor client. Direct-child close alone does not close the
2
+ // durable operation: require the nonce-bound ECHILD + pipe-drain report as well.
3
+ import {spawn} from 'node:child_process';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {randomBytes} from 'node:crypto';
6
+ const fail=c=>Object.assign(Error('DELIVERY_PROCESS_'+c),{drained:false});
7
+ const causes=new Set([null,'ABORTED','SPAWN_FAILED','ORPHANED_DESCENDANT','TIMEOUT','OUTPUT_LIMIT']);
8
+ export function validateProcessReport(raw,nonce,exitCode,signal){
9
+ let r;try{r=JSON.parse(raw);}catch{throw fail('REPORT_INVALID');}
10
+ if(!r||Object.keys(r).sort().join(',')!=='cause,code,drained,nonce,reaped,schema,signal,started,stdout'||exitCode!==0||signal!==null||r.schema!==1||r.nonce!==nonce||r.drained!==true||typeof r.started!=='boolean'||
11
+ !Number.isSafeInteger(r.reaped)||r.reaped<(r.started?1:0)||!causes.has(r.cause)||
12
+ !(r.code===null||Number.isSafeInteger(r.code)&&r.code>=0&&r.code<=255)||
13
+ !(r.signal===null||typeof r.signal==='string'&&/^SIG[A-Z0-9]{1,24}$/.test(r.signal))||
14
+ (r.code===null&&r.signal===null&&r.started)||r.code!==null&&r.signal!==null||
15
+ typeof r.stdout!=='string'||r.stdout.length>1398104||Buffer.from(r.stdout,'base64').toString('base64')!==r.stdout)throw fail('DRAIN_UNKNOWN');
16
+ if(!r.started&&(r.code!==null||r.signal!==null||r.reaped!==0||r.stdout!==''||!['ABORTED','SPAWN_FAILED'].includes(r.cause)))throw fail('REPORT_INVALID');
17
+ return {...r,stdout:Buffer.from(r.stdout,'base64')};
18
+ }
19
+ export function runSupervised(argv,{cwd,env,timeout=20,limit=1048576,signal,onSpawn,onDrained,receipt}={},io={}){
20
+ if((io.platform??process.platform)!=='linux')return Promise.reject(fail('PLATFORM_UNSUPPORTED'));
21
+ if(signal?.aborted)return Promise.reject(Object.assign(fail('ABORTED'),{drained:true}));
22
+ const nonce=receipt?.nonce??randomBytes(32).toString('hex'),request=JSON.stringify({argv,cwd,env,timeout,limit,nonce,parent_pid:process.pid,...(receipt?{receipt}: {})});
23
+ if(request.length>131072)return Promise.reject(fail('INPUT_LIMIT'));
24
+ return new Promise((resolve,reject)=>{
25
+ let child,output='',invalid=false;
26
+ const abort=()=>{try{child.kill('SIGTERM');}catch{invalid=true;}};
27
+ try{
28
+ child=(io.spawn??spawn)('/usr/bin/python3',['-I','-B',fileURLToPath(new URL('./delivery-process.py',import.meta.url))],{
29
+ cwd,env:{PATH:'/usr/bin:/bin',LC_ALL:'C'},stdio:['pipe','pipe','pipe']});
30
+ child.stdout.on('data',b=>{if(output.length+b.length>1500000){invalid=true;abort();}else output+=b.toString('utf8');});
31
+ child.stderr.on('data',()=>{}); // Never log inherited diagnostics/credentials.
32
+ child.on('error',()=>{invalid=true;});child.stdin.on('error',()=>{invalid=true;});
33
+ child.once('close',(code,closedSignal)=>{
34
+ signal?.removeEventListener('abort',abort);
35
+ try{if(invalid)throw fail('DRAIN_UNKNOWN');const r=validateProcessReport(output,nonce,code,closedSignal);
36
+ onDrained?.({code:r.code,signal:r.signal,started:r.started,cause:r.cause,reaped:r.reaped});resolve(r);
37
+ }catch{reject(fail('DRAIN_UNKNOWN'));}
38
+ });
39
+ if(Number.isSafeInteger(child.pid))onSpawn?.({pid:child.pid});
40
+ signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();
41
+ child.stdin.end(request);
42
+ }catch{
43
+ if(child){invalid=true;abort();}else reject(fail('START_FAILED'));
44
+ }
45
+ });
46
+ }
@@ -0,0 +1,228 @@
1
+ """Private Linux child-tree supervisor. No shell, retries, or PID-disappearance proof.
2
+
3
+ The parent journals the supervisor before use. Only this supervisor's ECHILD after
4
+ waitpid reaping, plus closed output pipes, proves its managed descendants drained.
5
+ An absent/invalid final report never proves drainage, even if this process exited.
6
+ """
7
+ import base64
8
+ import ctypes
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import re
13
+ import selectors
14
+ import signal
15
+ import stat
16
+ import subprocess
17
+ import sys
18
+ import time
19
+
20
+
21
+ def supervise(argv, cwd, env, timeout=20.0, limit=1048576, expected_parent=None):
22
+ if sys.platform != 'linux':
23
+ raise RuntimeError('PLATFORM_UNSUPPORTED')
24
+ libc = ctypes.CDLL(None, use_errno=True)
25
+ if libc.prctl(36, 1, 0, 0, 0) != 0: # PR_SET_CHILD_SUBREAPER
26
+ raise RuntimeError('SUBREAPER_UNAVAILABLE')
27
+ enabled = ctypes.c_int()
28
+ if libc.prctl(37, ctypes.byref(enabled), 0, 0, 0) != 0 or enabled.value != 1:
29
+ raise RuntimeError('SUBREAPER_UNAVAILABLE')
30
+ stopping = [False]
31
+ signal.signal(signal.SIGTERM, lambda *_: stopping.__setitem__(0, True))
32
+ signal.signal(signal.SIGINT, lambda *_: stopping.__setitem__(0, True))
33
+ parent = os.getppid()
34
+ if libc.prctl(1, signal.SIGTERM, 0, 0, 0) != 0: # PR_SET_PDEATHSIG
35
+ raise RuntimeError('PARENT_SIGNAL_UNAVAILABLE')
36
+ if os.getppid() != parent or expected_parent is not None and parent != expected_parent:
37
+ stopping[0] = True
38
+ if stopping[0]:
39
+ return dict(drained=True, started=False, code=None, signal=None, cause='ABORTED', stdout='', reaped=0)
40
+ child = None
41
+ pipes = selectors.DefaultSelector()
42
+ out, total, reaped, code, main_done = bytearray(), 0, 0, None, False
43
+ cause, kill_since = None, None
44
+ start = time.monotonic()
45
+ try:
46
+ try:
47
+ child = subprocess.Popen(argv, cwd=cwd, env=env, stdin=subprocess.DEVNULL,
48
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
49
+ shell=False, start_new_session=True, close_fds=True)
50
+ except OSError:
51
+ return dict(drained=True, started=False, code=None, signal=None, cause='SPAWN_FAILED', stdout='', reaped=0)
52
+ for stream, label in [(child.stdout, 'out'), (child.stderr, 'err')]:
53
+ os.set_blocking(stream.fileno(), False)
54
+ pipes.register(stream, selectors.EVENT_READ, label)
55
+ while True:
56
+ empty = False
57
+ while True:
58
+ try:
59
+ pid, status = os.waitpid(-1, os.WNOHANG)
60
+ except ChildProcessError: # ECHILD, not kill(pid, 0) or /proc absence.
61
+ empty = True
62
+ break
63
+ if pid == 0:
64
+ break
65
+ reaped += 1
66
+ if pid == child.pid:
67
+ code = os.waitstatus_to_exitcode(status)
68
+ child.returncode = code
69
+ main_done = True
70
+ else:
71
+ cause = cause or 'ORPHANED_DESCENDANT'
72
+ now = time.monotonic()
73
+ if stopping[0]:
74
+ cause = cause or 'ABORTED'
75
+ if now - start >= timeout:
76
+ cause = cause or 'TIMEOUT'
77
+ if main_done and not empty:
78
+ cause = cause or 'ORPHANED_DESCENDANT'
79
+ if cause and not empty:
80
+ kill_since = kill_since or now
81
+ # Only our unreaped direct/adopted children. Do not kill a cached
82
+ # process-group ID that could be reused after its leader was reaped.
83
+ try:
84
+ with open(f'/proc/self/task/{os.getpid()}/children', encoding='ascii') as f:
85
+ children = [int(x) for x in f.read().split()]
86
+ for pid in children:
87
+ try:
88
+ os.kill(pid, signal.SIGKILL)
89
+ except ProcessLookupError:
90
+ pass
91
+ except OSError:
92
+ cause = 'DRAIN_UNKNOWN'
93
+ break
94
+ if now - kill_since > 5:
95
+ cause = 'DRAIN_UNKNOWN'
96
+ break
97
+ for key, _ in pipes.select(0.02):
98
+ try:
99
+ data = os.read(key.fileobj.fileno(), 65536)
100
+ except BlockingIOError:
101
+ continue
102
+ if not data:
103
+ pipes.unregister(key.fileobj)
104
+ key.fileobj.close()
105
+ continue
106
+ total += len(data)
107
+ if total > limit:
108
+ cause = cause or 'OUTPUT_LIMIT'
109
+ elif key.data == 'out':
110
+ out.extend(data)
111
+ if empty and main_done and not pipes.get_map():
112
+ return dict(drained=True, started=True, code=code if code >= 0 else None,
113
+ signal=signal.Signals(-code).name if code < 0 else None,
114
+ cause=cause, stdout=base64.b64encode(out).decode('ascii'), reaped=reaped)
115
+ if now - start > timeout + 6:
116
+ cause = 'DRAIN_UNKNOWN'
117
+ break
118
+ return dict(drained=False, started=True, code=None, signal=None, cause='DRAIN_UNKNOWN', stdout='', reaped=reaped)
119
+ finally:
120
+ for key in list(pipes.get_map().values()):
121
+ key.fileobj.close()
122
+ pipes.close()
123
+
124
+
125
+ def receipt_directory(binding, nonce):
126
+ keys = {'schema', 'operation_id', 'action_id', 'epoch', 'intent_fingerprint',
127
+ 'plan_fingerprint', 'nonce', 'boot_id', 'directory', 'binding_hash'}
128
+ if not isinstance(binding, dict) or set(binding) != keys:
129
+ raise ValueError()
130
+ body = {k: v for k, v in binding.items() if k != 'binding_hash'}
131
+ encoded = json.dumps(body, sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode()
132
+ directory = binding['directory']
133
+ if (binding['schema'] != 1 or binding['nonce'] != nonce or
134
+ not re.fullmatch(r'delivery-op-[a-f0-9-]{36}', binding['operation_id']) or
135
+ not re.fullmatch(r'delivery-[a-f0-9-]{36}', binding['action_id']) or
136
+ type(binding['epoch']) is not int or not 1 <= binding['epoch'] <= 9007199254740991 or
137
+ any(not isinstance(binding[k], str) or not re.fullmatch(r'[a-f0-9]{64}', binding[k])
138
+ for k in ['intent_fingerprint', 'plan_fingerprint', 'nonce', 'binding_hash']) or
139
+ hashlib.sha256(encoded).hexdigest() != binding['binding_hash'] or
140
+ not isinstance(directory, dict) or set(directory) != {'path', 'device', 'inode'} or
141
+ not isinstance(directory['path'], str) or not directory['path'].startswith('/') or
142
+ len(directory['path']) > 2048 or os.path.realpath(directory['path']) != directory['path']):
143
+ raise ValueError()
144
+ with open('/proc/sys/kernel/random/boot_id', encoding='ascii') as source:
145
+ if source.read().strip() != binding['boot_id']:
146
+ raise ValueError()
147
+ fd = os.open(directory['path'], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
148
+ try:
149
+ current = os.fstat(fd)
150
+ if (not stat.S_ISDIR(current.st_mode) or current.st_mode & 0o077 or
151
+ current.st_uid != os.geteuid() or str(current.st_dev) != directory['device'] or
152
+ str(current.st_ino) != directory['inode']):
153
+ raise ValueError()
154
+ return fd
155
+ except Exception:
156
+ os.close(fd)
157
+ raise
158
+
159
+
160
+ def seal_receipt(fd, binding, result):
161
+ # Called only after supervise returned ECHILD + closed descendant pipes.
162
+ # No child is ever started after this point. Publish once, without replacing
163
+ # any existing report; fsync bytes and directory before stdout acknowledgement.
164
+ if result['drained'] is not True:
165
+ return
166
+ with open('/proc/self/stat', encoding='ascii') as source:
167
+ start_ticks = source.read().rsplit(')', 1)[1].strip().split()[19]
168
+ report = {k: v for k, v in result.items() if k != 'stdout'}
169
+ data = json.dumps({'schema': 1, 'kind': 'sealed-process-receipt-v1',
170
+ 'operation_id': binding['operation_id'], 'binding_hash': binding['binding_hash'],
171
+ 'boot_id': binding['boot_id'], 'pid': os.getpid(), 'start_ticks': start_ticks,
172
+ 'report': {'schema': 1, 'nonce': binding['nonce'], **report}},
173
+ sort_keys=True, separators=(',', ':')).encode()
174
+ if len(data) > 16384:
175
+ raise ValueError()
176
+ temporary, final = '.'+binding['nonce']+'.tmp', binding['nonce']+'.json'
177
+ output = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=fd)
178
+ try:
179
+ with os.fdopen(output, 'wb') as stream:
180
+ stream.write(data)
181
+ stream.flush()
182
+ os.fsync(stream.fileno())
183
+ os.link(temporary, final, src_dir_fd=fd, dst_dir_fd=fd, follow_symlinks=False)
184
+ os.unlink(temporary, dir_fd=fd)
185
+ os.fsync(fd)
186
+ except Exception:
187
+ # Retain an incomplete private file for diagnosis, never overwrite/adopt it.
188
+ raise
189
+
190
+
191
+ def main():
192
+ # Private IPC only. The public plugin accepts opaque plans, never argv/env.
193
+ receipt_fd = None
194
+ try:
195
+ raw = sys.stdin.buffer.read(131073)
196
+ if len(raw) > 131072:
197
+ raise ValueError()
198
+ request = json.loads(raw)
199
+ keys = {'argv', 'cwd', 'env', 'timeout', 'limit', 'nonce', 'parent_pid'}
200
+ if set(request) not in (keys, keys | {'receipt'}):
201
+ raise ValueError()
202
+ argv, env = request['argv'], request['env']
203
+ if (not isinstance(argv, list) or not 1 <= len(argv) <= 100 or
204
+ any(not isinstance(x, str) or '\0' in x or len(x) > 8192 for x in argv) or
205
+ not argv[0].startswith('/') or not isinstance(env, dict) or len(env) > 100 or
206
+ any(not isinstance(k, str) or not isinstance(v, str) or '=' in k or '\0' in k+v for k, v in env.items()) or
207
+ not isinstance(request['cwd'], str) or not request['cwd'].startswith('/') or
208
+ not 0.05 <= request['timeout'] <= 60 or not 1 <= request['limit'] <= 1048576 or
209
+ not isinstance(request['parent_pid'], int) or request['parent_pid'] < 1 or
210
+ not isinstance(request['nonce'], str) or not re.fullmatch(r'[a-f0-9]{64}', request['nonce'])):
211
+ raise ValueError()
212
+ if 'receipt' in request:
213
+ receipt_fd = receipt_directory(request['receipt'], request['nonce'])
214
+ result = supervise(argv, request['cwd'], env, request['timeout'], request['limit'], request['parent_pid'])
215
+ if receipt_fd is not None:
216
+ seal_receipt(receipt_fd, request['receipt'], result)
217
+ print(json.dumps({'schema': 1, 'nonce': request['nonce'], **result}), flush=True)
218
+ except Exception:
219
+ # Never expose argv, keys, environment, Git stderr or exception strings.
220
+ print(json.dumps({'schema': 1, 'drained': False, 'cause': 'SUPERVISOR_FAILED'}), flush=True)
221
+ raise SystemExit(1)
222
+ finally:
223
+ if receipt_fd is not None:
224
+ os.close(receipt_fd)
225
+
226
+
227
+ if __name__ == '__main__':
228
+ main()
@@ -0,0 +1,89 @@
1
+ // Separate push authority over a completed local commit. No Git/network effects.
2
+ // Reuses the same ledger and trusted decision verifier; a commit approval never
3
+ // authorizes push, and an old remote observation is not an execution preflight.
4
+ import {randomUUID} from 'node:crypto';
5
+ import {canonical,digest} from './approval-ledger.js';
6
+ const fail=c=>{throw Error('DELIVERY_PUSH_APPROVAL_'+c);};
7
+ const same=(a,b)=>canonical(a)===canonical(b);
8
+ const proofHash=r=>digest({task_id:r.task_id,spec_revision:r.spec_revision,task_hash:r.task_hash,proposal:JSON.parse(r.proposal),repository_revision:r.repository_revision,expires:r.expires});
9
+ export class DeliveryPushApproval {
10
+ constructor({commit,journal,enabled=false}){
11
+ this.commit=commit;this.j=journal;this.l=commit.l;this.db=this.l.db;this.enabled=enabled;
12
+ if(journal.db!==this.db)fail('DATABASE_MISMATCH');
13
+ this.db.exec(`CREATE TABLE IF NOT EXISTS delivery_push_plans(id TEXT PRIMARY KEY,event_id TEXT UNIQUE NOT NULL,
14
+ task_id TEXT NOT NULL,event_fingerprint TEXT NOT NULL,snapshot TEXT NOT NULL,created INTEGER NOT NULL);`);
15
+ }
16
+ source(task,action){
17
+ const r=this.j.row(action),i=this.j.intent(r),a=this.l.row(r.approval_id),p=JSON.parse(a.proposal),d=p.delivery;
18
+ if(r.task_id!==task||r.state!=='completed'||i.kind!=='commit-execution-intent-v1'||a.task_id!==task||
19
+ !same(p.scopes,['commit'])||d?.kind!=='commit-execution-v1'||proofHash(a)!==a.proposal_hash)fail('SOURCE_REQUIRED');
20
+ const s=this.commit.stored(task,d.plan_id,d.fingerprint);this.commit.current(s,a.spec_revision);
21
+ if(!same(this.commit.snapshot(s),d)||!same(s.intent,i)||!same(s.policy,p.model_policy)||!same(s.repository,p.repository)||
22
+ !this.l.repositories?.matches(task,a.repository_revision,p.repository)||p.repository?.provider!=='gitee'||
23
+ !Number.isSafeInteger(p.repository.repository_id)||p.repository.repository_id<1)fail('SOURCE_CHANGED');
24
+ return {row:r,intent:i,result:JSON.parse(r.result),repository:p.repository,policy:p.model_policy,approval_hash:a.proposal_hash};
25
+ }
26
+ stage({event_id,task_id,resource,workspace_identity,plan}){
27
+ if(this.enabled!==true)fail('DISABLED');
28
+ if(typeof event_id!=='string'||!event_id.trim()||event_id.length>256||/[\x00-\x1f\x7f]/.test(event_id))fail('INVALID_EVENT');
29
+ return this.l.store.tx(()=>{
30
+ const intent=this.j.prepare({task_id,resource,workspace_identity,plan}),event_fingerprint=digest(intent);
31
+ const old=this.db.prepare('SELECT * FROM delivery_push_plans WHERE event_id=?').get(event_id);
32
+ if(old){if(old.event_fingerprint!==event_fingerprint)fail('EVENT_CONFLICT');const s=this.stored(task_id,old.id,JSON.parse(old.snapshot).fingerprint);return {reference:this.reference(s),replayed:true,grants_execution:false};}
33
+ const source=this.source(task_id,plan.source_action_id),h=this.l.catalog.head(task_id);
34
+ if(h.legacy||this.l.store.task(task_id).blocked||source.repository.url!==resource||source.repository.repository_id!==plan.repository_id)fail('SOURCE_CHANGED');
35
+ this.j.readiness(intent); // A private, fresh, read-only preflight was primed before this transaction.
36
+ const body={schema:1,kind:'push-approval-plan-v1',id:'push-plan-'+randomUUID(),task_id,spec_revision:h.spec_revision,task_hash:digest(h.goal),
37
+ repository:structuredClone(source.repository),policy:structuredClone(source.policy),source_approval_hash:source.approval_hash,intent,created:this.l.now()};
38
+ const s={...body,fingerprint:digest(body)};
39
+ this.db.prepare('INSERT INTO delivery_push_plans VALUES (?,?,?,?,?,?)').run(s.id,event_id,task_id,event_fingerprint,canonical(s),s.created);
40
+ return {reference:this.reference(s),replayed:false,grants_execution:false};
41
+ });
42
+ }
43
+ reference(s){return {kind:'push-execution-reference-v1',plan_id:s.id,fingerprint:s.fingerprint};}
44
+ stored(task,id,fingerprint){
45
+ const row=this.db.prepare('SELECT * FROM delivery_push_plans WHERE id=?').get(id);if(!row||row.task_id!==task)fail('PLAN_NOT_FOUND');
46
+ const s=JSON.parse(row.snapshot),{fingerprint:hash,...body}=s;
47
+ if(s.id!==id||s.task_id!==task||s.kind!=='push-approval-plan-v1'||s.schema!==1||hash!==fingerprint||digest(body)!==hash)fail('PLAN_CHANGED');
48
+ if(!same(this.j.prepare(s.intent),s.intent))fail('SOURCE_CHANGED');return s;
49
+ }
50
+ current(s,revision){
51
+ const h=this.l.catalog.head(s.task_id),source=this.source(s.task_id,s.intent.plan.source_action_id);
52
+ if(h.spec_revision!==revision||s.spec_revision!==revision||digest(h.goal)!==s.task_hash||
53
+ !same(source.repository,s.repository)||!same(source.policy,s.policy)||source.approval_hash!==s.source_approval_hash)fail('SOURCE_CHANGED');
54
+ }
55
+ snapshot(s){
56
+ const p=s.intent.plan;
57
+ return {kind:'push-execution-v1',plan_id:s.id,fingerprint:s.fingerprint,intent_fingerprint:s.intent.fingerprint,plan_fingerprint:p.fingerprint,
58
+ source_action_id:p.source_action_id,source_approval_hash:s.source_approval_hash,task_revision:s.spec_revision,workspace_revision:s.intent.task_revision,
59
+ repository_id:p.repository_id,repository_url:p.url,repository_fingerprint:s.repository.fingerprint,workspace_branch:p.branch,ref:p.ref,
60
+ expected_remote_oid:p.expected_remote_oid,commit_oid:p.commit_oid,tree_oid:p.tree_oid,manifest_sha256:p.manifest_sha256,
61
+ tested_content:structuredClone(p.tested_content),prepared_at:s.created,execution:'requires_runtime_gate',max_pushes:1,
62
+ hooks:false,signed:false,rewrite:false,delete:false,tags:false,commit:false,publish:false,deploy:false};
63
+ }
64
+ approvalSnapshot(task,revision,reference,repository,policy){
65
+ if(reference.kind!=='push-execution-reference-v1')return this.commit.approvalSnapshot(task,revision,reference,repository,policy);
66
+ if(this.enabled!==true)fail('DISABLED');const s=this.stored(task,reference.plan_id,reference.fingerprint);this.current(s,revision);
67
+ if(!same(s.repository,repository)||!same(s.policy,policy))fail('BINDING_CHANGED');
68
+ // Human approval can arrive later. No network I/O or freshness renewal here;
69
+ // acquisition and transmission must obtain a new preflight for this exact baseline.
70
+ return this.snapshot(s);
71
+ }
72
+ matches(task,revision,plan){
73
+ if(plan?.kind!=='push-execution-v1')return this.commit.matches(task,revision,plan);
74
+ try{if(this.enabled!==true)return false;const s=this.stored(task,plan.plan_id,plan.fingerprint);this.current(s,revision);return same(this.snapshot(s),plan);}catch{return false;}
75
+ }
76
+ authorize(a){
77
+ if(a.intent?.kind!=='push-execution-intent-v1')return this.commit.authorize(a);
78
+ if(this.enabled!==true)fail('DISABLED');
79
+ return this.commit.readSnapshot(()=>{
80
+ const r=this.l.row(a.approval_id),v=this.l.view(r.id),p=v.proposal.delivery;
81
+ if(v.state!=='approved'||!same(v.proposal.scopes,['push'])||p?.kind!=='push-execution-v1'||proofHash(r)!==r.proposal_hash)fail('EXECUTION_APPROVAL_REQUIRED');
82
+ const s=this.stored(r.task_id,p.plan_id,p.fingerprint);
83
+ if(!same(s.intent,a.intent)||!same(s.policy,v.proposal.model_policy)||!same(s.repository,v.proposal.repository))fail('INTENT_CHANGED');
84
+ const proof=this.commit.decisionProof(r);
85
+ return {kind:'delivery-execution-v1',scope:'push',execution:true,approval_id:r.id,task_id:r.task_id,task_revision:a.intent.task_revision,
86
+ intent_fingerprint:a.intent.fingerprint,authority_hash:digest({approval_id:r.id,proposal_hash:r.proposal_hash,intent_fingerprint:a.intent.fingerprint,...proof})};
87
+ });
88
+ }
89
+ }
@@ -0,0 +1,98 @@
1
+ import {managedTaskBranch} from './task-branch.js';
2
+ // Internal push contract. No filesystem, process, credential or network access.
3
+ // A verified local commit is evidence, never authority to update a remote ref.
4
+ import {canonical,digest} from './approval-ledger.js';
5
+ import {repositoryURL} from './repository-gitee.js';
6
+ const fail=c=>{throw Error('DELIVERY_PUSH_'+c);};
7
+ const same=(a,b)=>canonical(a)===canonical(b);
8
+ const sha=x=>typeof x==='string'&&/^[a-f0-9]{64}$/.test(x);
9
+ const oid=x=>typeof x==='string'&&/^[a-f0-9]{40}$/.test(x);
10
+ const branch=managedTaskBranch;
11
+ const action=x=>typeof x==='string'&&/^delivery-[a-f0-9-]{36}$/.test(x);
12
+ const hashed=v=>{if(!v||typeof v!=='object'||Array.isArray(v))return false;const {fingerprint,...b}=v;return sha(fingerprint)&&digest(b)===fingerprint;};
13
+ const fields=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join(',')===keys.split(',').sort().join(',');
14
+ const keys='schema,kind,source_action_id,source_intent_fingerprint,source_result_fingerprint,repository_id,url,format,branch,ref,commit_oid,parent_oid,tree_oid,manifest_sha256,tested_content,expected_remote_oid,max_pushes,rewrite,delete,tags,publish,deploy,grants_execution,fingerprint';
15
+ export function validatePushPlan(p){
16
+ if(!fields(p,keys)||!hashed(p)||p.schema!==1||p.kind!=='push-commit-v1'||!action(p.source_action_id)||
17
+ !sha(p.source_intent_fingerprint)||!sha(p.source_result_fingerprint)||!Number.isSafeInteger(p.repository_id)||p.repository_id<1||
18
+ repositoryURL(p.url).url!==p.url||p.format!=='sha1'||!branch(p.branch)||p.ref!=='refs/heads/'+p.branch||
19
+ ![p.commit_oid,p.parent_oid,p.tree_oid].every(oid)||!sha(p.manifest_sha256)||
20
+ !(p.expected_remote_oid===null||oid(p.expected_remote_oid))||p.expected_remote_oid===p.commit_oid||
21
+ !fields(p.tested_content,'kind,sha256')||!['file_sha256','workspace_manifest_sha256'].includes(p.tested_content.kind)||!sha(p.tested_content.sha256)||
22
+ p.max_pushes!==1||[p.rewrite,p.delete,p.tags,p.publish,p.deploy,p.grants_execution].some(x=>x!==false))fail('PLAN_INVALID');
23
+ return p;
24
+ }
25
+ export function compilePush({source_action_id,commit_intent:i,commit_result:r,repository_id,expected_remote_oid}){
26
+ const p=i?.plan;
27
+ if(!action(source_action_id)||!hashed(i)||i.kind!=='commit-execution-intent-v1'||!hashed(p)||p.kind!=='commit-objects-v1'||
28
+ p.format!=='sha1'||p.persisted!==false||p.grants_execution!==false||r?.kind!=='commit-readback-v1'||r.drained!==true||
29
+ r.intent_fingerprint!==i.fingerprint||r.commit_oid!==p.computed_commit_oid||r.tree_oid!==p.tree_oid||r.branch!==p.branch||
30
+ r.manifest_sha256!==p.manifest_sha256||!same(r.tested_content,p.tested_content))fail('COMMIT_EVIDENCE_REQUIRED');
31
+ const body={schema:1,kind:'push-commit-v1',source_action_id,source_intent_fingerprint:i.fingerprint,source_result_fingerprint:digest(r),
32
+ repository_id,url:i.resource,format:p.format,branch:p.branch,ref:'refs/heads/'+p.branch,commit_oid:p.computed_commit_oid,
33
+ parent_oid:p.parent,tree_oid:p.tree_oid,manifest_sha256:p.manifest_sha256,tested_content:structuredClone(p.tested_content),
34
+ expected_remote_oid,max_pushes:1,rewrite:false,delete:false,tags:false,publish:false,deploy:false,grants_execution:false};
35
+ return validatePushPlan({...body,fingerprint:digest(body)});
36
+ }
37
+ export function pushIntent(store,{task_id,resource,workspace_identity,plan}){
38
+ validatePushPlan(plan);
39
+ const r=store.db.prepare('SELECT * FROM delivery_actions WHERE id=?').get(plan.source_action_id);
40
+ const i=r&&JSON.parse(r.intent),result=r?.result&&JSON.parse(r.result);
41
+ const e=r&&store.db.prepare("SELECT data FROM delivery_events WHERE action_id=? AND type='readback_completed' ORDER BY seq DESC LIMIT 1").get(r.id);
42
+ const proof=e&&JSON.parse(e.data);
43
+ if(!r||r.state!=='completed'||r.dispatched!==1||!Number.isSafeInteger(r.epoch)||r.task_id!==task_id||r.resource!==resource||
44
+ !hashed(i)||i.task_id!==task_id||i.resource!==resource||digest({event_id:r.event_id,approval_id:r.approval_id,intent:i})!==r.fingerprint||
45
+ proof?.epoch!==r.epoch||!same(proof.result,result)||store.db.prepare("SELECT 1 FROM delivery_operations WHERE action_id=? AND state<>'closed'").get(r.id)||
46
+ !same(compilePush({source_action_id:r.id,commit_intent:i,commit_result:result,repository_id:plan.repository_id,expected_remote_oid:plan.expected_remote_oid}),plan)||
47
+ !same(workspace_identity,i.workspace_identity)||resource!==plan.url||store.task(task_id).revision!==i.task_revision)fail('SOURCE_CHANGED');
48
+ const body={schema:1,kind:'push-execution-intent-v1',task_id,task_revision:i.task_revision,resource,
49
+ workspace_identity:structuredClone(workspace_identity),plan:structuredClone(plan)};
50
+ return validatePushIntent({...body,fingerprint:digest(body)});
51
+ }
52
+ export function validatePushIntent(i){
53
+ if(!fields(i,'schema,kind,task_id,task_revision,resource,workspace_identity,plan,fingerprint')||!hashed(i)||i.schema!==1||i.kind!=='push-execution-intent-v1'||
54
+ !Number.isSafeInteger(i.task_revision)||i.task_revision<0)fail('INTENT_INVALID');
55
+ validatePushPlan(i.plan);const w=i.workspace_identity;
56
+ if(i.resource!==i.plan.url||!fields(w,'root,url,branch,base_sha,directory,git_directory')||w.url!==i.resource||w.branch!==i.plan.branch||w.base_sha!==i.plan.parent_oid||
57
+ typeof w.root!=='string'||!w.root.startsWith('/')||w.root.length>2048||/[\x00-\x1f\x7f]/.test(w.root))fail('IDENTITY_INVALID');
58
+ for(const v of [w.directory,w.git_directory])if(!fields(v,'device,inode')||!/^\d{1,30}$/.test(v.device)||!/^\d{1,30}$/.test(v.inode))fail('IDENTITY_INVALID');
59
+ return i;
60
+ }
61
+ export function validatePushReadiness(i,r,now){
62
+ validatePushIntent(i);const p=i.plan,b=r?.baseline;
63
+ if(r?.kind!=='push-preflight-v1'||r.intent_fingerprint!==i.fingerprint||r.drained!==true||r.local_commit_verified!==true||
64
+ r.fast_forward_verified!==(p.expected_remote_oid!==null)||!Number.isSafeInteger(r.observed_at)||r.observed_at>now||now-r.observed_at>5000||
65
+ !fields(b,'kind,intent_fingerprint,workspace_identity,repository_id,url,ref,remote_oid,commit_oid,tree_oid,manifest_sha256,local_snapshot_sha256,fingerprint')||
66
+ !hashed(b)||b.kind!=='push-state-v1'||b.intent_fingerprint!==i.fingerprint||!same(b.workspace_identity,i.workspace_identity)||
67
+ b.repository_id!==p.repository_id||b.url!==p.url||b.ref!==p.ref||b.remote_oid!==p.expected_remote_oid||
68
+ b.commit_oid!==p.commit_oid||b.tree_oid!==p.tree_oid||b.manifest_sha256!==p.manifest_sha256||!sha(b.local_snapshot_sha256))fail('PREFLIGHT_REQUIRED');
69
+ return structuredClone(b);
70
+ }
71
+ export function pushReadback(i,proof,now){
72
+ validatePushIntent(i);const p=i.plan;
73
+ if(proof?.kind!=='push-readback-v1'||proof.intent_fingerprint!==i.fingerprint||proof.drained!==true||
74
+ !Number.isSafeInteger(proof.observed_at)||proof.observed_at>now||now-proof.observed_at>5000||
75
+ proof.repository_id!==p.repository_id||proof.url!==p.url||proof.ref!==p.ref||proof.ref_oid!==p.commit_oid||
76
+ !same(proof.workspace_identity,i.workspace_identity))fail('READBACK_MISMATCH');
77
+ return {kind:'push-readback-v1',intent_fingerprint:i.fingerprint,repository_id:p.repository_id,url:p.url,ref:p.ref,
78
+ commit_oid:p.commit_oid,expected_remote_oid:p.expected_remote_oid,drained:true,observed_at:proof.observed_at,
79
+ remote_acceptance:'ref_observed',runtime_adoption:'not_observed',publish:false,deploy:false};
80
+ }
81
+ // Command construction is not execution. The eventual transport must independently
82
+ // check ancestry before invoking the CAS lease: --force-with-lease alone permits rewrites.
83
+ export function pushCommand(p){
84
+ validatePushPlan(p);const r=repositoryURL(p.url);
85
+ return ['push','--porcelain','--no-verify','--no-follow-tags','--signed=false','--recurse-submodules=no',
86
+ '--force-with-lease='+p.ref+':'+(p.expected_remote_oid??''),'--','git@gitee.com:'+r.owner+'/'+r.repo+'.git',p.commit_oid+':'+p.ref];
87
+ }
88
+ export function remoteReadCommand(p){
89
+ validatePushPlan(p);const r=repositoryURL(p.url);
90
+ return ['ls-remote','--refs','--exit-code','--','git@gitee.com:'+r.owner+'/'+r.repo+'.git',p.ref];
91
+ }
92
+ export function parseRemoteRef(p,{code,stdout}){
93
+ validatePushPlan(p);
94
+ if(typeof stdout!=='string'||stdout.length>1024)fail('REMOTE_UNKNOWN');
95
+ if(code===2&&stdout==='')return null;
96
+ const m=/^([a-f0-9]{40})\t(refs\/heads\/[^\r\n]+)\n$/.exec(stdout);
97
+ if(code!==0||!m||m[2]!==p.ref)fail('REMOTE_UNKNOWN');return m[1];
98
+ }
@@ -0,0 +1,32 @@
1
+ // Operator-only reconciliation, never invoked by startup, model tools or a timer.
2
+ // Sealed process evidence can close an operation, not release a task or prove push.
3
+ const fail=c=>{throw Error('DELIVERY_PUSH_RECOVERY_'+c);};
4
+ export class DeliveryPushRecovery {
5
+ constructor({journal,receipts,executor,quiescent=()=>false,enabled=false}){Object.assign(this,{j:journal,receipts,executor,quiescent,enabled});}
6
+ guard(id,epoch){
7
+ if(this.enabled!==true)fail('DISABLED');if(this.quiescent()!==true)fail('NOT_QUIESCENT');
8
+ const r=this.j.fence(id,epoch,['needs_review']),i=this.j.intent(r);
9
+ if(i.kind!=='push-execution-intent-v1'||!r.dispatched||this.j.store.task(r.task_id).revision!==i.task_revision)fail('ACTION_INVALID');return i;
10
+ }
11
+ inspect(id,epoch){
12
+ this.guard(id,epoch);
13
+ return {action_id:id,epoch,operations:this.j.db.prepare('SELECT id,state FROM delivery_operations WHERE action_id=? ORDER BY rowid').all(id).map(o=>{
14
+ try{return {...o,evidence:this.receipts.inspect(o.id)};}catch{return {...o,evidence:null,error:'PROCESS_RECEIPT_UNVERIFIED'};}
15
+ }),remote_checked_now:false,task_released:false,retry_authorized:false};
16
+ }
17
+ closeOperation(id,epoch,operation_id,fingerprint){
18
+ this.guard(id,epoch);const o=this.j.operation(operation_id);
19
+ if(o.action_id!==id||o.epoch!==epoch)fail('OPERATION_MISMATCH');
20
+ return this.receipts.reconcile(operation_id,fingerprint);
21
+ }
22
+ async settle(id,epoch,{signal}={}){
23
+ const i=this.guard(id,epoch);if(this.j.unfinished(id))fail('DRAIN_REQUIRED');
24
+ try{
25
+ await this.executor.readback(i,{signal});this.guard(id,epoch);
26
+ return {...this.j.finish(id,epoch),retry_authorized:false};
27
+ }catch{
28
+ // Retain uncertainty without acquiring a new generation or repeating a send.
29
+ this.guard(id,epoch);this.j.event(id,'remote_readback_failed',{epoch,error:'REMOTE_READBACK_UNVERIFIED'});return {action_id:id,state:'needs_review',error:'REMOTE_READBACK_UNVERIFIED',task_released:false,retry_authorized:false};
30
+ }
31
+ }
32
+ }