@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,5 @@
1
+ # Official bundle selection loads the native setup owner and its declared Web client.
2
+ # Empty configuration discovers capabilities; it never consents to mutations.
3
+ - insert:
4
+ - id: dual-host-onboarding
5
+ name: '@mogragsigma/dsh-dual-host-plugin'
@@ -0,0 +1,133 @@
1
+ // Ledger bridge. Constructed only by the opt-in delivery runtime; never writes Git.
2
+ // Stage is operator/runtime-only: public proposals accept an opaque reference, not objects or approval flags.
3
+ import {randomUUID} from 'node:crypto';
4
+ import {canonical,digest} from './approval-ledger.js';
5
+ import {verifyObjects} from './delivery-object-plan.js';
6
+ const fail=code=>{throw Error('DELIVERY_APPROVAL_'+code);};
7
+ 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});
8
+ const same=(a,b)=>canonical(a)===canonical(b);
9
+ export class DeliveryApproval {
10
+ constructor({ledger,preview,journal,enabled=false}){
11
+ this.l=ledger;this.db=ledger.db;this.preview=preview;this.j=journal;this.enabled=enabled;
12
+ if(journal.db!==this.db||preview.db!==this.db)fail('DATABASE_MISMATCH');
13
+ this.db.exec(`CREATE TABLE IF NOT EXISTS delivery_approval_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
+ this.db.exec(`CREATE TABLE IF NOT EXISTS delivery_plan_objects(plan_id TEXT NOT NULL,ordinal INTEGER NOT NULL,
16
+ oid TEXT NOT NULL,type TEXT NOT NULL,bytes BLOB NOT NULL,PRIMARY KEY(plan_id,ordinal),UNIQUE(plan_id,oid));`);
17
+ }
18
+ sourceEvidence(p){
19
+ // Completed development is historical evidence. Superseding its approval must not erase it.
20
+ const r=this.db.prepare('SELECT * FROM development_requests WHERE id=?').get(p.request_id);
21
+ if(!r||r.task_id!==p.task_id||r.state!=='completed'||r.approval_id!==p.source_approval.id||r.expected_hash!==p.tested_content.sha256)fail('SOURCE_CHANGED');
22
+ const a=this.l.row(r.approval_id);if(a.task_id!==p.task_id||a.spec_revision!==p.spec_revision||a.proposal_hash!==p.source_approval.proposal_hash||proofHash(a)!==a.proposal_hash)fail('SOURCE_CHANGED');
23
+ const operations=this.db.prepare('SELECT * FROM development_operations WHERE request_id=? ORDER BY id').all(r.id);
24
+ const writes=this.db.prepare('SELECT * FROM development_writes WHERE request_id=? ORDER BY id').all(r.id);
25
+ if(operations.some(o=>o.state!=='closed')||writes.some(w=>w.state!=='applied'))fail('SOURCE_CHANGED');
26
+ const commands=this.db.prepare('SELECT * FROM development_commands WHERE request_id=? ORDER BY id').all(r.id);
27
+ const safe=this.db.prepare("SELECT data FROM development_events WHERE request_id=? AND type='safe_point' ORDER BY seq DESC LIMIT 1").get(r.id);
28
+ return digest({request:r,operations,writes,commands,safe,source_approval_hash:a.proposal_hash});
29
+ }
30
+ stage({event_id,task_id,preview_id,fingerprint,resource,workspace_identity,plan,objects}){
31
+ if(this.enabled!==true)fail('DISABLED');
32
+ if(typeof event_id!=='string'||!event_id.trim()||event_id.length>256||/[\x00-\x1f\x7f]/.test(event_id))fail('INVALID_EVENT');
33
+ return this.l.store.tx(()=>{
34
+ const intent=this.j.prepare({task_id,resource,workspace_identity,plan,objects});
35
+ const event_fingerprint=digest({task_id,preview_id,fingerprint,intent});
36
+ const old=this.db.prepare('SELECT * FROM delivery_approval_plans WHERE event_id=?').get(event_id);
37
+ 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};}
38
+ const p=this.preview.stored(task_id,preview_id,fingerprint),head=this.l.catalog.head(task_id);
39
+ if(head.legacy||head.spec_revision!==p.spec_revision||this.l.store.task(task_id).blocked)fail('TASK_CHANGED');
40
+ const checked=this.preview.checked(p),changes=p.files.map(f=>({path:f.path,action:f.approved_action,before_sha256:f.before_sha256,after_sha256:f.after_sha256})).sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
41
+ if(p.repository.url!==resource||p.workspace_branch!==plan.branch||p.baseline_commit!==plan.parent||
42
+ !same({kind:p.tested_content.kind,sha256:p.tested_content.sha256},plan.tested_content)||!same(changes,plan.changes)||!same(checked.w.identity(),workspace_identity))fail('PREVIEW_MISMATCH');
43
+ // The private read adapter re-compiles against actual bytes; preview metadata alone is insufficient.
44
+ this.j.readiness(intent);
45
+ const body={schema:1,kind:'commit-approval-plan-v1',id:'commit-plan-'+randomUUID(),task_id,spec_revision:p.spec_revision,task_hash:digest(head.goal),
46
+ preview_id:p.id,preview_fingerprint:p.fingerprint,repository:structuredClone(p.repository),policy:structuredClone(checked.a.proposal.model_policy),
47
+ source_evidence_hash:this.sourceEvidence(p),intent,created:this.l.now()};
48
+ const s={...body,fingerprint:digest(body)};
49
+ this.db.prepare('INSERT INTO delivery_approval_plans VALUES (?,?,?,?,?,?)').run(s.id,event_id,task_id,event_fingerprint,canonical(s),s.created);
50
+ const put=this.db.prepare('INSERT INTO delivery_plan_objects VALUES (?,?,?,?,?)');
51
+ objects.forEach((o,n)=>put.run(s.id,n,o.oid,o.type,Buffer.from(o.data)));
52
+ return {reference:this.reference(s),replayed:false,grants_execution:false};
53
+ });
54
+ }
55
+ reference(s){return {kind:'commit-execution-reference-v1',plan_id:s.id,fingerprint:s.fingerprint};}
56
+ objects(s){
57
+ // Durable exact bytes allow queue admission while the workspace is busy. No
58
+ // regeneration from mutable worktree contents, and no caller-supplied objects.
59
+ const rows=this.db.prepare('SELECT oid,type,bytes FROM delivery_plan_objects WHERE plan_id=? ORDER BY ordinal').all(s.id);
60
+ const objects=rows.map(o=>({oid:o.oid,type:o.type,data:Buffer.from(o.bytes)}));
61
+ verifyObjects(s.intent.plan,objects);return objects;
62
+ }
63
+ stored(task,id,fingerprint){
64
+ const row=this.db.prepare('SELECT * FROM delivery_approval_plans WHERE id=?').get(id);if(!row||row.task_id!==task)fail('PLAN_NOT_FOUND');
65
+ const s=JSON.parse(row.snapshot),{fingerprint:hash,...body}=s,{fingerprint:ih,...ib}=s.intent??{}, {fingerprint:ph,...pb}=s.intent?.plan??{};
66
+ if(s.id!==id||s.task_id!==task||s.kind!=='commit-approval-plan-v1'||s.schema!==1||hash!==fingerprint||digest(body)!==hash||digest(ib)!==ih||digest(pb)!==ph)fail('PLAN_CHANGED');return s;
67
+ }
68
+ current(s,revision){
69
+ const h=this.l.catalog.head(s.task_id),p=this.preview.stored(s.task_id,s.preview_id,s.preview_fingerprint);
70
+ if(s.spec_revision!==revision||h.spec_revision!==revision||digest(h.goal)!==s.task_hash||
71
+ this.l.store.task(s.task_id).revision!==s.intent.task_revision||this.sourceEvidence(p)!==s.source_evidence_hash)fail('SOURCE_CHANGED');
72
+ return p;
73
+ }
74
+ snapshot(s){
75
+ const p=s.intent.plan;
76
+ return {kind:'commit-execution-v1',plan_id:s.id,fingerprint:s.fingerprint,intent_fingerprint:s.intent.fingerprint,plan_fingerprint:p.fingerprint,
77
+ preview_id:s.preview_id,preview_fingerprint:s.preview_fingerprint,task_revision:s.spec_revision,workspace_revision:s.intent.task_revision,
78
+ repository_fingerprint:s.repository.fingerprint,workspace_branch:p.branch,baseline_commit:p.parent,computed_commit_oid:p.computed_commit_oid,
79
+ tree_oid:p.tree_oid,format:p.format,manifest_sha256:p.manifest_sha256,tested_content:structuredClone(p.tested_content),
80
+ files_digest:digest(p.changes),file_count:p.changes.length,author:structuredClone(p.author),committer:structuredClone(p.committer),
81
+ timestamp:p.when,commit_message:p.message,source_evidence_hash:s.source_evidence_hash,execution:'requires_runtime_gate',
82
+ hooks:false,filters:false,signed:false,max_commits:1,push:false,publish:false,deploy:false};
83
+ }
84
+ approvalSnapshot(task,revision,reference,repository,policy){
85
+ if(reference.kind!=='commit-execution-reference-v1')return this.preview.approvalSnapshot(task,revision,reference,repository,policy);
86
+ if(this.enabled!==true)fail('DISABLED');const s=this.stored(task,reference.plan_id,reference.fingerprint),p=this.current(s,revision);
87
+ if(!same(s.repository,repository)||!same(s.policy,policy))fail('BINDING_CHANGED');
88
+ this.preview.checked(p);this.j.readiness(s.intent);return this.snapshot(s);
89
+ }
90
+ matches(task,revision,plan){
91
+ if(plan?.kind!=='commit-execution-v1')return this.preview.matches(task,revision,plan);
92
+ try{const s=this.stored(task,plan.plan_id,plan.fingerprint);this.current(s,revision);return same(this.snapshot(s),plan);}catch{return false;}
93
+ }
94
+ decisionProof(r){
95
+ const d=this.db.prepare('SELECT * FROM approval_decisions WHERE id=?').get(r.decision_id);
96
+ if(!d||d.request_id!==r.id||d.action!=='approve'||!d.principal)fail('PROOF_REQUIRED');
97
+ if(d.credential_id?.startsWith('chat:')){
98
+ const proof=this.db.prepare('SELECT * FROM approval_chat_proofs WHERE challenge_id=?').get(d.challenge_id);
99
+ const b=proof&&this.db.prepare('SELECT * FROM approval_chat_bindings WHERE id=? AND disabled=0').get(proof.binding_hash);
100
+ const route=this.db.prepare('SELECT * FROM approval_chat_deliveries WHERE request_id=?').get(r.id);
101
+ if(!proof||!b||!route)fail('PROOF_REQUIRED');const p=JSON.parse(proof.proof),binding=JSON.parse(b.binding);
102
+ if(proof.proof_hash!==d.proof_hash||digest(p)!==d.proof_hash||d.credential_id!=='chat:'+b.id||digest(binding)!==b.id||
103
+ p.provider!=='feishu'||p.binding_hash!==b.id||p.request_id!==r.id||p.proposal_hash!==r.proposal_hash||p.actor!==binding.approverOpenId||
104
+ d.principal!==`feishu:${binding.appId}:${binding.approverOpenId}`||p.message_id!==proof.message_id||p.parent_id!==route.message_id||
105
+ p.prompt_version!==route.prompt_version||route.binding_hash!==b.id||route.state!=='sent'||
106
+ (p.interpretation?(p.interpretation.intent!=='approve'||p.interpretation.unconditional!==true):!['确定','/yes'].includes(p.text?.trim())))fail('PROOF_REQUIRED');
107
+ }else{
108
+ const c=this.db.prepare('SELECT * FROM approval_credentials WHERE id=? AND disabled=0').get(d.credential_id);
109
+ const q=this.db.prepare('SELECT * FROM approval_challenges WHERE id=?').get(d.challenge_id);
110
+ if(!c||c.principal!==d.principal||q?.kind!=='decision'||q.used!==1)fail('PROOF_REQUIRED');
111
+ const binding=JSON.parse(q.binding),result=JSON.parse(q.result??'null');
112
+ if(binding.request_id!==r.id||binding.action!=='approve'||binding.proposal_hash!==r.proposal_hash||result?.proof_hash!==d.proof_hash)fail('PROOF_REQUIRED');
113
+ }
114
+ return {decision_id:d.id,proof_hash:d.proof_hash};
115
+ }
116
+ readSnapshot(fn){
117
+ // Can run inside the journal's write transaction. A nested BEGIN would break enqueue/acquire.
118
+ const name='delivery_authority_'+randomUUID().replaceAll('-','');this.db.exec('SAVEPOINT '+name);
119
+ try{const result=fn();this.db.exec('RELEASE '+name);return result;}catch(e){this.db.exec('ROLLBACK TO '+name);this.db.exec('RELEASE '+name);throw e;}
120
+ }
121
+ authorize({approval_id,intent}){
122
+ if(this.enabled!==true)fail('DISABLED');
123
+ return this.readSnapshot(()=>{
124
+ const r=this.l.row(approval_id),v=this.l.view(approval_id),p=v.proposal.delivery;
125
+ if(v.state!=='approved'||!same(v.proposal.scopes,['commit'])||p?.kind!=='commit-execution-v1'||proofHash(r)!==r.proposal_hash)fail('EXECUTION_APPROVAL_REQUIRED');
126
+ const s=this.stored(r.task_id,p.plan_id,p.fingerprint);
127
+ if(!same(s.intent,intent)||!same(s.policy,v.proposal.model_policy)||!same(s.repository,v.proposal.repository))fail('INTENT_CHANGED');
128
+ const proof=this.decisionProof(r);
129
+ return {kind:'delivery-execution-v1',scope:'commit',execution:true,approval_id:r.id,task_id:r.task_id,task_revision:intent.task_revision,
130
+ intent_fingerprint:intent.fingerprint,authority_hash:digest({approval_id:r.id,proposal_hash:r.proposal_hash,intent_fingerprint:intent.fingerprint,...proof})};
131
+ });
132
+ }
133
+ }
@@ -0,0 +1,52 @@
1
+ // A durable push journal is the registry. Read-only derived entries survive a
2
+ // crash without a second INSERT/HTTP transaction and never renew task binding.
3
+ import {canonical,digest} from './approval-ledger.js';
4
+ import {deliveryAction} from './delivery-state.js';
5
+ import {repositoryURL} from './repository-gitee.js';
6
+ const same=(a,b)=>canonical(a)===canonical(b);
7
+ const has=(db,t)=>!!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(t);
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
+ function authority(db,row,intent,scope){
10
+ const a=db.prepare('SELECT * FROM approval_requests WHERE id=? AND task_id=?').get(row.approval_id,row.task_id);
11
+ if(!a||proofHash(a)!==a.proposal_hash)return null;
12
+ const p=JSON.parse(a.proposal),d=p.delivery;
13
+ if(!same(p.scopes,[scope])||d?.kind!==scope+'-execution-v1'||d.intent_fingerprint!==intent.fingerprint||d.plan_fingerprint!==intent.plan.fingerprint)return null;
14
+ // Historical authority, not a new grant. Later expiry/revocation cannot erase
15
+ // an already observed effect; the exact approving decision must still exist.
16
+ const decisions=db.prepare("SELECT id,proof_hash FROM approval_decisions WHERE request_id=? AND action='approve' ORDER BY created DESC LIMIT 100").all(a.id);
17
+ if(!decisions.some(x=>digest({approval_id:a.id,proposal_hash:a.proposal_hash,intent_fingerprint:intent.fingerprint,decision_id:x.id,proof_hash:x.proof_hash})===row.authority_hash))return null;
18
+ return {a,p};
19
+ }
20
+ export function deliveryArtifact(g,task,id){
21
+ if(!/^artifact-delivery-[a-f0-9-]{36}$/.test(id??''))return null;
22
+ const db=g.store?.db,action=id.slice('artifact-'.length);
23
+ if(!db||!['delivery_actions','approval_requests','approval_decisions'].every(t=>has(db,t)))return null;
24
+ try{
25
+ const view=deliveryAction(g,task,action);if(view?.kind!=='push'||view.state!=='completed'||view.remote_acceptance!=='ref_observed')return null;
26
+ const row=db.prepare('SELECT * FROM delivery_actions WHERE id=? AND task_id=?').get(action,task),i=JSON.parse(row.intent),p=i.plan;
27
+ const source=db.prepare('SELECT * FROM delivery_actions WHERE id=? AND task_id=?').get(p.source_action_id,task),commit=deliveryAction(g,task,p.source_action_id);
28
+ if(!source||commit?.state!=='completed'||commit.commit_oid!==p.commit_oid)return null;
29
+ const ci=JSON.parse(source.intent),cp=ci.plan,cr=JSON.parse(source.result);
30
+ if(ci.fingerprint!==p.source_intent_fingerprint||digest(cr)!==p.source_result_fingerprint||cp.tree_oid!==p.tree_oid||cp.parent!==p.parent_oid||
31
+ cp.branch!==p.branch||ci.resource!==i.resource||!same(ci.workspace_identity,i.workspace_identity)||!Array.isArray(cp.changes)||!cp.changes.length||cp.changes.length>128)return null;
32
+ const pushAuth=authority(db,row,i,'push'),commitAuth=authority(db,source,ci,'commit');
33
+ if(!pushAuth||!commitAuth||pushAuth.a.spec_revision!==commitAuth.a.spec_revision||!same(pushAuth.p.repository,commitAuth.p.repository))return null;
34
+ const binding=pushAuth.p.repository;
35
+ if(binding?.provider!=='gitee'||binding.repository_id!==p.repository_id||binding.url!==repositoryURL(p.url).url||binding.base_sha!==p.parent_oid||
36
+ pushAuth.a.repository_revision!==binding.binding_revision||commitAuth.p.delivery.files_digest!==digest(cp.changes))return null;
37
+ const changes={create:0,modify:0,delete:0};for(const f of cp.changes){if(!Object.hasOwn(changes,f.action))return null;changes[f.action]++;}
38
+ const proof=digest({push_intent:i.fingerprint,push_result:JSON.parse(row.result),commit_intent:ci.fingerprint,commit_result:cr,
39
+ push_authority:row.authority_hash,commit_authority:source.authority_hash,repository:binding,spec_revision:pushAuth.a.spec_revision});
40
+ return {id,task_id:task,kind:'gitee_delivery',name:'交付成果 · '+p.commit_oid.slice(0,12),
41
+ description:`本任务已推送 ${cp.changes.length} 项文件变更(新增 ${changes.create}、修改 ${changes.modify}、删除 ${changes.delete})到 ${p.branch}。入口为固定提交的完整变更页;不代表发布、部署或整项长期任务完成。`,
42
+ spec_revision:pushAuth.a.spec_revision,created_at:view.remote_observed_at,repository:{...binding,base_sha:p.commit_oid,branch:p.branch},
43
+ repository_origin:binding,delivery:{action_id:action,commit_action_id:source.id,commit_oid:p.commit_oid,tree_oid:p.tree_oid,parent_oid:p.parent_oid,changes,proof_hash:proof},size:null,sha256:proof};
44
+ }catch{return null;}
45
+ }
46
+ export function deliveryArtifactList(g,task){
47
+ const db=g.store?.db;if(!db||!has(db,'delivery_actions'))return {items:[],total:0};
48
+ const where="task_id=? AND state='completed' AND json_extract(intent,'$.kind')='push-execution-intent-v1'";
49
+ try{const rows=db.prepare('SELECT id FROM delivery_actions WHERE '+where+' ORDER BY seq DESC LIMIT 100').all(task);
50
+ return {items:rows.map(r=>deliveryArtifact(g,task,'artifact-'+r.id)).filter(Boolean),total:db.prepare('SELECT count(*) n FROM delivery_actions WHERE '+where).get(task).n};
51
+ }catch{return {items:[],total:0,error:'ARTIFACT_DELIVERY_EVIDENCE_UNAVAILABLE'};}
52
+ }
@@ -0,0 +1,125 @@
1
+ import {managedTaskBranch} from './task-branch.js';
2
+ // Internal actual commit adapter, disabled by default and not wired to a public entry.
3
+ // Unit tests replace both Git and filesystem effects; no real Git-write acceptance yet.
4
+ import {constants,openSync,closeSync,fstatSync,lstatSync,realpathSync,writeSync,fsyncSync,ftruncateSync,renameSync,unlinkSync} from 'node:fs';
5
+ import {join} from 'node:path';
6
+ import {createHash} from 'node:crypto';
7
+ import {canonical} from './approval-ledger.js';
8
+ import {verifyObjects} from './delivery-object-plan.js';
9
+ import {compileIndex} from './delivery-index.js';
10
+ import {DeliveryGitRead} from './delivery-git-read.js';
11
+ import {execFileDrained} from './drained-process.js';
12
+ const fail=code=>{throw Error('DELIVERY_COMMIT_'+code);};
13
+ const sha=b=>createHash('sha256').update(b).digest('hex');
14
+ const syncDir=p=>{const fd=openSync(p,'r');try{fsyncSync(fd);}finally{closeSync(fd);}};
15
+ const statId=s=>({device:String(s.dev),inode:String(s.ino)});
16
+ const writeAll=(fd,data)=>{let at=0;while(at<data.length){const n=writeSync(fd,data,at,data.length-at,at);if(n<1)fail('SHORT_WRITE');at+=n;}};
17
+ export class DeliveryGitLease {
18
+ constructor(identity,tag,onLock=()=>{}){this.identity=structuredClone(identity);this.gd=join(identity.root,'.git');this.tag=tag;this.onLock=onLock;this.handles=new Map();}
19
+ checkRoot(){
20
+ for(const [path,id] of [[this.identity.root,this.identity.directory],[this.gd,this.identity.git_directory]]){
21
+ const s=lstatSync(path,{bigint:true});if(!s.isDirectory()||s.isSymbolicLink()||realpathSync(path)!==path||canonical(statId(s))!==canonical(id))fail('DIRECTORY_CHANGED');
22
+ }
23
+ }
24
+ assert(){this.checkRoot();for(const [name,h] of this.handles){
25
+ const s=lstatSync(join(this.gd,name),{bigint:true}),fd=fstatSync(h.fd,{bigint:true});
26
+ if(!s.isFile()||s.isSymbolicLink()||s.nlink!==1n||canonical(statId(s))!==canonical(h.identity)||canonical(statId(fd))!==canonical(h.identity))fail('LOCK_CHANGED');
27
+ }}
28
+ proofs(){return Object.fromEntries([...this.handles].map(([n,h])=>[n,{...h.identity}]));}
29
+ open(){
30
+ this.checkRoot();if(this.handles.size)fail('LEASE_ALREADY_OPEN');
31
+ // Do not hold HEAD.lock: Git may need it for a branch transaction. HEAD is rechecked;
32
+ // standard checkout is excluded by index.lock, not by an OS isolation guarantee.
33
+ for(const name of ['config.lock','index.lock']){
34
+ this.assert();const fd=openSync(join(this.gd,name),constants.O_RDWR|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600);
35
+ let identity;try{identity=statId(fstatSync(fd,{bigint:true}));}catch(e){closeSync(fd);throw e;}this.handles.set(name,{fd,identity});
36
+ writeAll(fd,Buffer.from(canonical(this.tag)+'\n'));fsyncSync(fd);syncDir(this.gd);this.onLock(name,identity);
37
+ }this.assert();
38
+ }
39
+ installIndex(bytes){
40
+ this.assert();const h=this.handles.get('index.lock');if(!h||!Buffer.isBuffer(bytes))fail('INDEX_LOCK_REQUIRED');
41
+ ftruncateSync(h.fd,0);writeAll(h.fd,bytes);fsyncSync(h.fd);this.assert();
42
+ renameSync(join(this.gd,'index.lock'),join(this.gd,'index'));
43
+ this.handles.delete('index.lock');try{closeSync(h.fd);}finally{syncDir(this.gd);}
44
+ }
45
+ release(){
46
+ this.assert();for(const [name,h] of [...this.handles].reverse()){
47
+ this.assert();unlinkSync(join(this.gd,name));this.handles.delete(name);try{closeSync(h.fd);}finally{syncDir(this.gd);}
48
+ }
49
+ }
50
+ abandon(){for(const h of this.handles.values()){try{closeSync(h.fd);}catch{}}this.handles.clear();} // Never unlink an uncertain lock.
51
+ }
52
+ export function commitCommand(kind,plan,object){
53
+ const oid=x=>typeof x==='string'&&new RegExp('^[a-f0-9]{'+(plan.format==='sha256'?64:40)+'}$').test(x);
54
+ if(!['sha1','sha256'].includes(plan?.format)||!managedTaskBranch(plan.branch)||!oid(plan.parent)||!oid(plan.computed_commit_oid))fail('COMMAND_DENIED');
55
+ if(kind==='object'&&object&&Buffer.isBuffer(object.data)&&['blob','tree','commit'].includes(object.type)&&plan.objects.some(o=>o.oid===object.oid&&o.type===object.type&&o.bytes===object.data?.length))return ['hash-object','-w','-t',object.type,'--stdin','--no-filters'];
56
+ if(kind==='reference'&&object===undefined)return ['update-ref','--no-deref','-m','DSH approved delivery','refs/heads/'+plan.branch,plan.computed_commit_oid,plan.parent];
57
+ fail('COMMAND_DENIED');
58
+ }
59
+ export class DeliveryCommit {
60
+ constructor({journal,binding,enabled=false,externalDrained=()=>false},io={}){
61
+ this.j=journal;this.enabled=enabled;this.binding=structuredClone(binding);this.active=false;this.lease=null;this.action=null;
62
+ this.reader=io.reader??new DeliveryGitRead({binding,drained:()=>externalDrained()===true&&(!this.action||!journal.unfinished(this.action)),ownedLocks:()=>this.lease?.proofs()??{}});
63
+ this.makeLease=io.makeLease??((identity,tag,onLock)=>new DeliveryGitLease(identity,tag,onLock));
64
+ this.runGit=io.runGit??((args,input,signal,observer,plan)=>execFileDrained('/usr/bin/git',[
65
+ '--git-dir='+join(binding.root,'.git'),'--work-tree='+binding.root,
66
+ '-c','core.hooksPath=/dev/null','-c','core.fsmonitor=false','-c','core.untrackedCache=false','-c','gc.auto=0','-c','maintenance.auto=false',
67
+ '-c','core.fsync=loose-object,reference','-c','core.fsyncMethod=fsync',...args],{
68
+ cwd:binding.root,timeout:10000,maxBuffer:1048576,killSignal:'SIGKILL',signal,
69
+ env:{PATH:'/usr/bin:/bin',LC_ALL:'C',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_SYSTEM:'/dev/null',GIT_CONFIG_GLOBAL:'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_REPLACE_OBJECTS:'1',GIT_TERMINAL_PROMPT:'0',GIT_LITERAL_PATHSPECS:'1',
70
+ GIT_COMMITTER_NAME:plan.committer.name,GIT_COMMITTER_EMAIL:plan.committer.email,GIT_COMMITTER_DATE:'@'+plan.when+' +0000'}},input,observer));
71
+ }
72
+ fsOperation(id,epoch,kind,fn){
73
+ const op=this.j.startOperation(id,epoch,kind);let code=1,async=false;try{const result=fn();if(result&&typeof result.then==='function'){async=true;fail('ASYNC_FS_DENIED');}code=0;return result;}finally{if(!async)this.j.closed(op,{code});}
74
+ }
75
+ async gitOperation(id,epoch,kind,plan,object,signal){
76
+ const args=commitCommand(kind,plan,object),op=this.j.startOperation(id,epoch,kind,object?{oid:object.oid}:{});
77
+ let output;try{output=await this.runGit(args,object?.data,signal,{
78
+ onSpawn:({pid})=>{if(pid!==null)this.j.spawned(op,pid);},onClose:result=>this.j.closed(op,result)},plan);
79
+ }catch(e){
80
+ const r=this.j.operation(op);if(e?.drained===true&&r.state==='intent'&&r.pid===null)this.j.closed(op,{code:null});throw e;
81
+ }
82
+ const closed=this.j.operation(op);if(closed.state!=='closed')fail('DRAIN_UNKNOWN');if(closed.code!==0||closed.signal!==null)fail('PROCESS_FAILED');
83
+ if(kind==='object'&&Buffer.from(output??'').toString('ascii').trim()!==object.oid)fail('OBJECT_RESULT_MISMATCH');
84
+ }
85
+ guard(id,epoch,intent,baseline,index,phase){
86
+ this.j.effectAuthority(id,epoch);if(this.j.unfinished(id))fail('DRAIN_UNKNOWN');this.lease.assert();
87
+ const s=this.reader.capture(intent),p=intent.plan,updated=phase==='referenced'||phase==='indexed';
88
+ if(s.head_oid!==(updated?p.computed_commit_oid:p.parent)||s.ref_oid!==(updated?p.computed_commit_oid:p.parent)||
89
+ s.metadata.config_sha256!==baseline.metadata.config_sha256||s.metadata.head_sha256!==baseline.metadata.head_sha256||
90
+ s.index_tree_oid!==(phase==='indexed'?p.tree_oid:baseline.parent_tree_oid)||
91
+ s.metadata.index_sha256!==(phase==='indexed'?sha(index.bytes):baseline.metadata.index_sha256))fail('STATE_CHANGED');
92
+ if(phase==='locked'&&s.fingerprint!==baseline.fingerprint)fail('BEFORE_SNAPSHOT_CHANGED');
93
+ if(phase!=='locked'&&s.objects_present.length!==p.objects.length)fail('OBJECTS_INCOMPLETE');
94
+ }
95
+ async execute(id,epoch,objects,{signal}={}){
96
+ if(this.enabled!==true)fail('DISABLED');if(this.active)fail('IN_FLIGHT');if(signal?.aborted)fail('CANCELLED');
97
+ this.active=true;let dispatched=false,intent;
98
+ try{
99
+ const row=this.j.fence(id,epoch);if(row.dispatched)fail('ALREADY_DISPATCHED');intent=this.j.intent(row);
100
+ if(this.binding.root!==intent.workspace_identity.root||this.binding.url!==intent.resource||this.binding.branch!==intent.plan.branch||this.binding.base_sha!==intent.plan.parent)fail('BINDING_CHANGED');
101
+ const copy=objects.map(o=>({oid:o.oid,type:o.type,data:Buffer.from(o.data)}));verifyObjects(intent.plan,copy);const index=compileIndex(intent.plan,copy);
102
+ const baseline=JSON.parse(row.before_snapshot??'null');this.j.markDispatched(id,epoch);dispatched=true;this.action=id;
103
+ this.j.event(id,'index_planned',{epoch,plan_fingerprint:intent.plan.fingerprint,tree_oid:index.tree_oid,sha256:sha(index.bytes),bytes:index.bytes.length});
104
+ this.lease=this.makeLease(intent.workspace_identity,{action_id:id,epoch,intent_fingerprint:intent.fingerprint},(name,identity)=>this.j.event(id,'lock_owned',{epoch,name,identity}));
105
+ this.fsOperation(id,epoch,'lock',()=>this.lease.open());this.guard(id,epoch,intent,baseline,index,'locked');
106
+ const present=new Set(baseline.objects_present);
107
+ for(const object of copy){
108
+ if(present.has(object.oid))continue;if(signal?.aborted)fail('CANCELLED');
109
+ this.j.effectAuthority(id,epoch);this.lease.assert();await this.gitOperation(id,epoch,'object',intent.plan,object,signal);
110
+ }
111
+ this.guard(id,epoch,intent,baseline,index,'objects');if(signal?.aborted)fail('CANCELLED');
112
+ await this.gitOperation(id,epoch,'reference',intent.plan,undefined,signal);
113
+ this.guard(id,epoch,intent,baseline,index,'referenced');if(signal?.aborted)fail('CANCELLED');
114
+ this.fsOperation(id,epoch,'index',()=>this.lease.installIndex(index.bytes));this.guard(id,epoch,intent,baseline,index,'indexed');
115
+ this.fsOperation(id,epoch,'unlock',()=>this.lease.release());this.reader.observe(intent);return this.j.finish(id,epoch);
116
+ }catch(e){
117
+ if(!dispatched)throw e;
118
+ // Cleanup only our inode-bound locks after all operations are closed. Revocation
119
+ // stops new effects but does not prevent this cleanup; uncertain child => keep locks.
120
+ if(this.lease&&!this.j.unfinished(id))try{this.fsOperation(id,epoch,'unlock',()=>this.lease.release());}catch{}
121
+ try{this.j.recordFailure(id,epoch);}catch{} // A lost owner never steals the new task fence.
122
+ return {...this.j.view(id),review_required:true,error:'DELIVERY_COMMIT_UNCERTAIN'};
123
+ }finally{this.lease?.abandon();this.lease=null;this.action=null;this.active=false;}
124
+ }
125
+ }
@@ -0,0 +1,42 @@
1
+ // Opaque input only: all file, branch and baseline identities come from the
2
+ // immutable server preview. This is an approval record, never an execution API.
3
+ export function deliveryReference(v){
4
+ if(['commit-execution-reference-v1','push-execution-reference-v1'].includes(v?.kind)){
5
+ const prefix=v.kind==='push-execution-reference-v1'?'push':'commit';
6
+ if(Object.keys(v).sort().join(',')!=='fingerprint,kind,plan_id'||!new RegExp('^'+prefix+'-plan-[a-f0-9-]{36}$').test(v.plan_id??'')||!/^[a-f0-9]{64}$/.test(v.fingerprint??''))throw Error('DELIVERY_INVALID_APPROVAL_REFERENCE');
7
+ return {kind:v.kind,plan_id:v.plan_id,fingerprint:v.fingerprint};
8
+ }
9
+ if(!v||typeof v!=='object'||Array.isArray(v)||Object.keys(v).some(k=>!['preview_id','fingerprint','commit_message'].includes(k))||typeof v.preview_id!=='string'||!/^delivery-[a-f0-9-]{36}$/.test(v.preview_id)||!/^[a-f0-9]{64}$/.test(v.fingerprint??'')||typeof v.commit_message!=='string'||!v.commit_message.trim()||v.commit_message.length>2000||/[\x00-\x09\x0b-\x1f\x7f]/.test(v.commit_message))throw Error('DELIVERY_INVALID_APPROVAL_REFERENCE');
10
+ return {preview_id:v.preview_id,fingerprint:v.fingerprint,commit_message:v.commit_message};
11
+ }
12
+ const previewReferenceSchema={type:'object',properties:{preview_id:{type:'string'},fingerprint:{type:'string'},commit_message:{type:'string',minLength:1,maxLength:2000}},required:['preview_id','fingerprint','commit_message'],additionalProperties:false};
13
+ export function deliveryLabel(p){
14
+ if(p.kind==='push-execution-v1')return [
15
+ '限定推送授权:仅将已完成本地提交推送至下列唯一远端分支;批准不等于已发送或已被远端接受。',
16
+ '仓库:'+p.repository_url+'(仓库 ID '+p.repository_id+')','目标分支:'+p.workspace_branch,
17
+ '远端预期旧值:'+(p.expected_remote_oid??'准备时确认该分支不存在'),'已完成本地提交:'+p.commit_oid,'目标树:'+p.tree_oid,
18
+ '测试内容:'+p.tested_content.kind+' / '+p.tested_content.sha256,'准备时间:'+new Date(p.prepared_at).toISOString(),
19
+ '审批计划指纹:'+p.fingerprint,
20
+ '远端旧值是准备时的观察,不保证仍然新鲜。执行前重新核对仓库身份、引用、祖先关系、工作区和进程排空,并以精确旧值条件更新。',
21
+ '最多发送一次;不重写历史、不删除引用、不推标签,不执行 hooks、不生成推送签名。发送未知不得重试,须独立回读核对。',
22
+ '不包含新建提交、开发修改、发布或部署;远端引用已匹配也不代表运行环境已经采用。'
23
+ ].join('\n');
24
+ if(p.kind==='commit-execution-v1')return [
25
+ '限定提交授权:仅云端独立工作区的一次本地提交;批准不等于已执行。',
26
+ '目标分支:'+p.workspace_branch,'预期父提交:'+p.baseline_commit,
27
+ '计算提交OID(尚不证明落盘):'+p.computed_commit_oid,'目标树:'+p.tree_oid,
28
+ '作者:'+JSON.stringify(p.author),'提交者:'+JSON.stringify(p.committer),'固定提交时间:'+new Date(p.timestamp*1000).toISOString(),
29
+ '精确文件集合:'+p.file_count+' 项;摘要 '+p.files_digest,'测试内容:'+p.tested_content.kind+' / '+p.tested_content.sha256,
30
+ '提交说明:'+JSON.stringify(p.commit_message),'审批计划指纹:'+p.fingerprint,
31
+ '请按任务名称查看对应完整预览;摘要不代表已读全文。仅绑定所列内容和元数据,不授权修改文件。',
32
+ '该适配不执行 Git hooks/filters,不生成签名;引用和索引不是同一原子操作,部分结果须待核对。',
33
+ '实际执行仍须单次额度、排队持权、现场与进程排空门禁;不包含推送、发布或部署。'
34
+ ].join('\n');
35
+ return `拟提交交付(仅记账,不执行):${p.workspace_branch}\n基线提交:${p.baseline_commit}\n测试内容 SHA256:${p.tested_content_sha256}\n精确文件集合:${p.file_count} 项,其中 ${p.changed_files} 项变化;集合摘要 ${p.files_digest}\n提交说明:${JSON.stringify(p.commit_message)}\n预览指纹:${p.fingerprint}\n请在双端工作台按任务名称查看对应预览和完整文件分页;摘要不代表全文已审阅。批准绑定上述内容,不是当前工作区的持锁保证;审批查询不会自动重验文件。提交执行器关闭,不包含推送、发布或部署。`;
36
+ }
37
+
38
+ export const deliveryReferenceSchema={oneOf:[previewReferenceSchema,{type:'object',properties:{kind:{enum:['commit-execution-reference-v1','push-execution-reference-v1']},plan_id:{type:'string'},fingerprint:{type:'string'}},required:['kind','plan_id','fingerprint'],additionalProperties:false}]};
39
+ export const commitExecution=p=>p?.delivery?.kind==='commit-execution-v1';
40
+ export const pushExecution=p=>p?.delivery?.kind==='push-execution-v1';
41
+ export const deliveryExecution=p=>commitExecution(p)||pushExecution(p);
42
+ export const approvalTitle=p=>pushExecution(p)?'【DSH 方案确认 · 限定推送授权】':commitExecution(p)?'【DSH 方案确认 · 限定提交授权】':p.development?'【DSH 方案确认 · 受限开发授权】':'【DSH 方案确认 · 当前仅记账,不执行】';
@@ -0,0 +1,217 @@
1
+ import {managedTaskBranch} from './task-branch.js';
2
+ // Private bounded read-only Git observer. No command here can update an object, ref or index.
3
+ import {execFileSync} from 'node:child_process';
4
+ import {constants,lstatSync,fstatSync,realpathSync,openSync,readSync,closeSync,readdirSync} from 'node:fs';
5
+ import {join} from 'node:path';
6
+ import {createHash} from 'node:crypto';
7
+ import {canonical,digest} from './approval-ledger.js';
8
+ import {developmentPath} from './development-plan.js';
9
+ import {repositoryURL,repositoryBranch} from './repository-gitee.js';
10
+ import {compileCommit,gitObject,indexTree} from './delivery-object-plan.js';
11
+ const fail=code=>{throw Error('DELIVERY_GIT_'+code);};
12
+ const sha=b=>createHash('sha256').update(b).digest('hex');
13
+ const same=(a,b)=>canonical(a)===canonical(b);
14
+ const validOID=x=>typeof x==='string'&&/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(x);
15
+ const stamp=s=>[String(s.dev),String(s.ino),String(s.size),String(s.mtimeNs),String(s.ctimeNs)].join(':');
16
+ const exists=p=>{try{return lstatSync(p);}catch(e){if(e.code==='ENOENT')return null;throw e;}};
17
+ function bytes(path,limit){
18
+ const before=lstatSync(path,{bigint:true});
19
+ if(!before.isFile()||before.nlink!==1n||before.size>BigInt(limit)||realpathSync(path)!==path)fail('FILE_UNSAFE');
20
+ const fd=openSync(path,constants.O_RDONLY|constants.O_NOFOLLOW);
21
+ try{
22
+ if(stamp(fstatSync(fd,{bigint:true}))!==stamp(before))fail('FILE_CHANGED');
23
+ const b=Buffer.alloc(Number(before.size)+1);let n=0,got;
24
+ while(n<b.length&&(got=readSync(fd,b,n,b.length-n,null))>0)n+=got;
25
+ if(n!==Number(before.size)||stamp(fstatSync(fd,{bigint:true}))!==stamp(before)||stamp(lstatSync(path,{bigint:true}))!==stamp(before))fail('FILE_CHANGED');
26
+ return b.subarray(0,n);
27
+ }finally{closeSync(fd);}
28
+ }
29
+ function physicalIdentity(b){
30
+ const dir=p=>{const s=lstatSync(p,{bigint:true});if(!s.isDirectory()||s.isSymbolicLink()||realpathSync(p)!==p)fail('DIRECTORY_UNSAFE');return {device:String(s.dev),inode:String(s.ino)};};
31
+ return {root:b.root,directory:dir(b.root),git_directory:dir(join(b.root,'.git')),url:b.url,branch:b.branch,base_sha:b.base_sha};
32
+ }
33
+ function inventory(root){
34
+ const out=[];let total=0;
35
+ const walk=(dir,prefix='')=>{
36
+ const before=lstatSync(dir,{bigint:true});if(!before.isDirectory()||realpathSync(dir)!==dir)fail('DIRECTORY_UNSAFE');
37
+ for(const name of readdirSync(dir).sort()){
38
+ if(!prefix&&name==='.git')continue;const path=prefix+name;developmentPath(path);const absolute=join(dir,name),s=lstatSync(absolute);
39
+ if(s.isSymbolicLink())fail('FILE_UNSAFE');if(s.isDirectory()){walk(absolute,path+'/');continue;}
40
+ const data=bytes(absolute,8388608);total+=data.length;if(total>33554432||out.length>=4096)fail('LIMIT');
41
+ out.push({path,bytes:data,mode:s.mode&0o111?0o755:0o644});
42
+ }
43
+ if(stamp(lstatSync(dir,{bigint:true}))!==stamp(before))fail('DIRECTORY_CHANGED');
44
+ };walk(root);return out;
45
+ }
46
+ function metadata(root,owned={}){
47
+ const gd=join(root,'.git');
48
+ for(const p of ['commondir','gitdir','shallow','info/grafts','objects/info/alternates','objects/info/http-alternates','refs/replace','worktrees','reftable'])if(exists(join(gd,p)))fail('REPOSITORY_LAYOUT_UNSUPPORTED');
49
+ if(Object.keys(owned).some(p=>!['index.lock','config.lock'].includes(p)))fail('LOCK_PROOF_INVALID');
50
+ const own=p=>{const expected=Object.hasOwn(owned,p)?owned[p]:null;if(!expected)return false;const s=lstatSync(join(gd,p),{bigint:true});
51
+ if(!s.isFile()||s.isSymbolicLink()||s.nlink!==1n||String(s.dev)!==expected.device||String(s.ino)!==expected.inode)fail('LOCK_PROOF_CHANGED');return true;};
52
+ for(const p of ['index.lock','HEAD.lock','config.lock'])if(!own(p)&&exists(join(gd,p)))fail('LOCKED');
53
+ // Reject links/hardlinks before Git can follow object, pack or reference paths.
54
+ let count=0;const layout=[];const walk=(dir,depth=0)=>{if(depth>64)fail('LIMIT');for(const name of readdirSync(dir).sort()){
55
+ if(++count>65536)fail('LIMIT');const p=join(dir,name),s=lstatSync(p);
56
+ if(dir===gd&&own(name))continue;if(name.endsWith('.lock'))fail('LOCKED');
57
+ if(s.isSymbolicLink()||(!s.isDirectory()&&(!s.isFile()||s.nlink!==1)))fail('REPOSITORY_LAYOUT_UNSUPPORTED');
58
+ layout.push({path:p.slice(gd.length+1),kind:s.isDirectory()?'directory':'file',mode:s.mode&0o777,size:s.isDirectory()?null:s.size});
59
+ if(s.isDirectory())walk(p,depth+1);
60
+ }};walk(gd);
61
+ return {layout_sha256:digest(layout),config_sha256:sha(bytes(join(gd,'config'),262144)),index_sha256:exists(join(gd,'index'))?sha(bytes(join(gd,'index'),8388608)):null,head_sha256:sha(bytes(join(gd,'HEAD'),2048))};
62
+ }
63
+ function config(raw,url){
64
+ const seen=new Set();let origin=false;
65
+ const values={
66
+ 'core.repositoryformatversion':/^[01]$/,'core.filemode':/^(true|false)$/,'core.bare':/^false$/,
67
+ 'core.logallrefupdates':/^(true|false)$/,'core.ignorecase':/^(true|false)$/,'core.precomposeunicode':/^(true|false)$/,
68
+ 'extensions.objectformat':/^(sha1|sha256)$/,'remote.origin.fetch':/^\+refs\/heads\/\*:refs\/remotes\/origin\/\*$/,
69
+ 'user.name':/^[^\x00-\x1f\x7f]{1,256}$/,'user.email':/^[^\x00-\x1f\x7f]{1,254}$/
70
+ };
71
+ for(const item of raw.toString('utf8').split('\0').filter(Boolean)){
72
+ const at=item.indexOf('\n');if(at<1)fail('CONFIG_UNSUPPORTED');const key=item.slice(0,at),value=item.slice(at+1);
73
+ if(seen.has(key))fail('CONFIG_UNSUPPORTED');seen.add(key);
74
+ if(key==='remote.origin.url'){try{if(repositoryURL(value).url!==url)fail('REMOTE_CHANGED');}catch{fail('REMOTE_CHANGED');}origin=true;}
75
+ else if(key==='remote.origin.fetch'&&value!=='+refs/heads/*:refs/remotes/origin/*'){
76
+ const m=/^\+refs\/heads\/(.+):refs\/remotes\/origin\/(.+)$/.exec(value);
77
+ if(!m||m[1]!==m[2])fail('CONFIG_UNSUPPORTED');
78
+ try{repositoryBranch(m[1]);}catch{fail('CONFIG_UNSUPPORTED');}
79
+ }else if(/^branch\.(.+)\.(remote|merge)$/.test(key)){
80
+ const m=/^branch\.(.+)\.(remote|merge)$/.exec(key);
81
+ try{repositoryBranch(m[1]);if(m[2]==='remote'?value!=='origin':!value.startsWith('refs/heads/'))fail('CONFIG_UNSUPPORTED');if(m[2]==='merge')repositoryBranch(value.slice(11));}catch{fail('CONFIG_UNSUPPORTED');}
82
+ }else if(!Object.hasOwn(values,key)||!values[key].test(value))fail('CONFIG_UNSUPPORTED');
83
+ }
84
+ if(!origin)fail('REMOTE_CHANGED');
85
+ }
86
+ function entries(raw,index=false){
87
+ const out=[],str=raw.toString('utf8');if(!Buffer.from(str).equals(raw))fail('PATH_ENCODING');
88
+ for(const record of str.split('\0').filter(Boolean)){
89
+ const m=(index?/^(100644|100755) ([a-f0-9]{40}|[a-f0-9]{64}) 0\t(.+)$/u:/^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/u).exec(record);
90
+ if(!m)fail('TREE_UNSUPPORTED');developmentPath(m[3]);out.push({mode:m[1]==='100755'?0o755:0o644,oid:m[2],path:m[3]});
91
+ }return out;
92
+ }
93
+ export function parseBatch(raw,oids,format){
94
+ const out=new Map();let at=0;
95
+ for(const oid of oids){
96
+ const end=raw.indexOf(10,at);if(end<0)fail('OBJECT_INVALID');const h=raw.subarray(at,end).toString('ascii');at=end+1;
97
+ if(h===oid+' missing'){out.set(oid,null);continue;}
98
+ const m=/^([a-f0-9]+) (blob|tree|commit) ([0-9]+)$/.exec(h);if(!m||m[1]!==oid)fail('OBJECT_INVALID');
99
+ const size=Number(m[3]);if(!Number.isSafeInteger(size)||size>40000000||at+size>=raw.length||raw[at+size]!==10)fail('OBJECT_INVALID');
100
+ const obj=gitObject(m[2],raw.subarray(at,at+size),format);if(obj.oid!==oid)fail('OBJECT_INVALID');out.set(oid,obj);at+=size+1;
101
+ }if(at!==raw.length)fail('OBJECT_INVALID');return out;
102
+ }
103
+ export function readOnlyCommand(kind,arg){
104
+ const simple={config:['config','--local','--no-includes','--null','--list'],format:['rev-parse','--show-object-format'],branch:['symbolic-ref','--quiet','HEAD'],head:['rev-parse','--verify','HEAD'],index:['ls-files','--stage','-z'],flags:['ls-files','-v','-z'],batch:['cat-file','--batch']};
105
+ if(Object.hasOwn(simple,kind)&&arg===undefined)return simple[kind];
106
+ if(kind==='ref'&&managedTaskBranch(arg))return ['rev-parse','--verify','refs/heads/'+arg];
107
+ if(validOID(arg)&&kind==='tree')return ['ls-tree','-r','-z',arg];
108
+ if(validOID(arg)&&kind==='root-tree')return ['rev-parse','--verify',arg+'^{tree}'];
109
+ fail('READ_COMMAND_DENIED');
110
+ }
111
+ export function validateGitBaseline(intent,s){
112
+ const {fingerprint,...body}=s??{},p=intent.plan;
113
+ if(!fingerprint||digest(body)!==fingerprint||s.kind!=='git-state-v1'||s.intent_fingerprint!==intent.fingerprint||
114
+ !same(s.workspace_identity,intent.workspace_identity)||s.branch!==p.branch||s.head_oid!==p.parent||s.ref_oid!==p.parent||
115
+ !validOID(s.parent_tree_oid)||s.index_tree_oid!==s.parent_tree_oid||s.manifest_sha256!==p.manifest_sha256||
116
+ !s.metadata||Object.keys(s.metadata).sort().join(',')!=='config_sha256,head_sha256,index_sha256,layout_sha256'||
117
+ Object.entries(s.metadata).some(([k,v])=>k==='index_sha256'&&v===null?s.index_tree_oid!==indexTree([],p.format):typeof v!=='string'||!/^[a-f0-9]{64}$/.test(v))||
118
+ !Array.isArray(s.objects_present)||new Set(s.objects_present).size!==s.objects_present.length||
119
+ !same(s.objects_present,[...s.objects_present].sort())||s.objects_present.some(oid=>!p.objects.some(o=>o.oid===oid)))fail('BASELINE_REQUIRED');
120
+ return s;
121
+ }
122
+ export class DeliveryGitRead {
123
+ constructor({binding,drained=()=>false,ownedLocks=()=>({})},io={}){
124
+ if(!binding||typeof binding.root!=='string'||!binding.root.startsWith('/')||binding.root.endsWith('/')||repositoryURL(binding.url).url!==binding.url||
125
+ !managedTaskBranch(binding.branch)||!validOID(binding.base_sha))fail('BINDING');
126
+ this.b=structuredClone(binding);this.drained=drained;
127
+ this.identity=()=>io.identity?io.identity():physicalIdentity(this.b);
128
+ this.inventory=()=>io.inventory?io.inventory():inventory(this.b.root);
129
+ this.metadata=()=>io.metadata?io.metadata():metadata(this.b.root,ownedLocks());
130
+ this.execute=io.execute??((args,input)=>execFileSync('/usr/bin/git',args,{cwd:this.b.root,input,timeout:10000,maxBuffer:67108864,
131
+ env:{PATH:'/usr/bin:/bin',GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_SYSTEM:'/dev/null',GIT_CONFIG_GLOBAL:'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_REPLACE_OBJECTS:'1',GIT_TERMINAL_PROMPT:'0',GIT_LITERAL_PATHSPECS:'1',LC_ALL:'C'},stdio:['pipe','pipe','pipe']}));
132
+ }
133
+ read(kind,arg,input){
134
+ const command=readOnlyCommand(kind,arg);
135
+ if(kind!=='batch'&&input!==undefined)fail('READ_COMMAND_DENIED');
136
+ if(kind==='batch'&&(!Buffer.isBuffer(input)||input.length>5000000||!input.toString('ascii').split('\n').filter(Boolean).every(validOID)))fail('READ_COMMAND_DENIED');
137
+ try{return Buffer.from(this.execute(['--git-dir='+join(this.b.root,'.git'),'--work-tree='+this.b.root,
138
+ '-c','core.hooksPath=/dev/null','-c','core.fsmonitor=false','-c','core.untrackedCache=false','-c','gc.auto=0','-c','maintenance.auto=false',...command],input));}
139
+ catch{fail('READ_FAILED');}
140
+ }
141
+ objects(oids,format){if(!oids.length)return new Map();return parseBatch(this.read('batch',undefined,Buffer.from(oids.join('\n')+'\n')),oids,format);}
142
+ ancestor(older,newer,format='sha1'){
143
+ if(!['sha1','sha256'].includes(format)||![older,newer].every(v=>typeof v==='string'&&new RegExp('^[a-f0-9]{'+(format==='sha1'?40:64)+'}$').test(v)))fail('ANCESTRY_INPUT');
144
+ if(this.drained()!==true)fail('DRAIN_REQUIRED');
145
+ const seen=new Set(),queue=[newer];let found=false;
146
+ // Read verified raw commit objects, not rev-list helpers, replacements or network fetches.
147
+ for(let at=0;at<queue.length;at++){
148
+ const id=queue[at];if(seen.has(id))continue;if(seen.size>=16384)fail('ANCESTRY_LIMIT');seen.add(id);
149
+ const object=this.objects([id],format).get(id);if(object?.type!=='commit')fail('ANCESTRY_OBJECT');
150
+ const end=object.data.indexOf(Buffer.from('\n\n'));if(end<0||end>1048576)fail('ANCESTRY_OBJECT');
151
+ const lines=object.data.subarray(0,end).toString('utf8').split('\n'),oid=new RegExp('^[a-f0-9]{'+(format==='sha1'?40:64)+'}$');
152
+ if(!lines[0].startsWith('tree ')||!oid.test(lines[0].slice(5)))fail('ANCESTRY_OBJECT');
153
+ const parents=lines.filter(x=>x.startsWith('parent ')).map(x=>x.slice(7));
154
+ if(parents.length>16||parents.some(x=>!oid.test(x)))fail('ANCESTRY_OBJECT');
155
+ if(id===older){found=true;break;}queue.push(...parents);
156
+ }
157
+ if(this.drained()!==true)fail('DRAIN_REQUIRED');return found;
158
+ }
159
+ compile(input){
160
+ // Inputs are resolved by the runtime from an immutable, checked preview and
161
+ // operator-configured author identity. This reads only; staging rechecks the
162
+ // complete preflight again before persisting the approval plan.
163
+ if(this.drained()!==true)fail('DRAIN_REQUIRED');
164
+ if(input.parent!==this.b.base_sha||input.branch!==this.b.branch)fail('BINDING');
165
+ const identity=this.identity(),meta=this.metadata();config(this.read('config'),this.b.url);
166
+ const format=this.read('format').toString('ascii').trim();
167
+ if(this.read('branch').toString('ascii').trim()!=='refs/heads/'+this.b.branch||
168
+ this.read('head').toString('ascii').trim()!==input.parent||this.read('ref',this.b.branch).toString('ascii').trim()!==input.parent)fail('BASELINE_CHANGED');
169
+ const base=entries(this.read('tree',input.parent)),raw=this.objects([...new Set(base.map(x=>x.oid))],format);
170
+ if(this.read('root-tree',input.parent).toString('ascii').trim()!==indexTree(base,format))fail('BASELINE_TREE_UNSUPPORTED');
171
+ const baseline=base.map(x=>{const o=raw.get(x.oid);if(o?.type!=='blob')fail('BASELINE_OBJECT');return {path:x.path,mode:x.mode,bytes:o.data};});
172
+ const compiled=compileCommit({...input,format,baseline,files:this.inventory()});
173
+ if(!same(identity,this.identity())||!same(meta,this.metadata())||this.drained()!==true)fail('STATE_CHANGED');
174
+ return {...compiled,workspace_identity:identity};
175
+ }
176
+ capture(intent){
177
+ const {fingerprint,...body}=intent??{},p=intent?.plan,{fingerprint:pf,...pb}=p??{};
178
+ if(intent?.kind!=='commit-execution-intent-v1'||digest(body)!==fingerprint||digest(pb)!==pf||p.kind!=='commit-objects-v1'||p.persisted!==false||p.grants_execution!==false)fail('INTENT_CHANGED');
179
+ if(this.drained()!==true)fail('DRAIN_REQUIRED');
180
+ const identity=this.identity(),meta=this.metadata();if(!same(identity,intent.workspace_identity)||this.b.url!==intent.resource||this.b.branch!==p.branch||this.b.base_sha!==p.parent)fail('WORKSPACE_CHANGED');
181
+ config(this.read('config'),this.b.url);const format=this.read('format').toString('ascii').trim();if(format!==p.format)fail('FORMAT_CHANGED');
182
+ const branch=this.read('branch').toString('ascii').trim();if(branch!=='refs/heads/'+p.branch)fail('BRANCH_CHANGED');
183
+ const head=this.read('head').toString('ascii').trim(),ref=this.read('ref',p.branch).toString('ascii').trim();if(!validOID(head)||!validOID(ref))fail('REFERENCE_INVALID');
184
+ const baseEntries=entries(this.read('tree',p.parent)),baseObjects=this.objects([...new Set(baseEntries.map(x=>x.oid))],format);
185
+ if(this.read('root-tree',p.parent).toString('ascii').trim()!==indexTree(baseEntries,format))fail('BASELINE_TREE_UNSUPPORTED');
186
+ const baseline=baseEntries.map(x=>{const o=baseObjects.get(x.oid);if(!o||o.type!=='blob')fail('BASELINE_OBJECT');return {path:x.path,mode:x.mode,bytes:o.data};});
187
+ const actual=this.inventory(),compiled=compileCommit({format,parent:p.parent,branch:p.branch,baseline,files:actual,changes:p.changes,tested_content:p.tested_content,author:p.author,committer:p.committer,when:p.when,message:p.message});
188
+ if(compiled.plan.fingerprint!==p.fingerprint)fail('PLAN_CHANGED');
189
+ const index=entries(this.read('index'),true),tree=indexTree(index,format),flags=this.read('flags').toString('utf8').split('\0').filter(Boolean);
190
+ if(flags.length!==index.length||!same(flags.map(x=>{if(!x.startsWith('H '))fail('INDEX_FLAGS_UNSUPPORTED');return x.slice(2);}).sort(),index.map(x=>x.path).sort()))fail('INDEX_FLAGS_UNSUPPORTED');
191
+ const objects=this.objects(p.objects.map(x=>x.oid),format);
192
+ for(const o of p.objects){const got=objects.get(o.oid);if(got&&(got.type!==o.type||got.data.length!==o.bytes))fail('OBJECT_INVALID');}
193
+ if(!same(identity,this.identity())||!same(meta,this.metadata())||this.read('head').toString('ascii').trim()!==head||
194
+ this.read('ref',p.branch).toString('ascii').trim()!==ref||this.read('branch').toString('ascii').trim()!==branch||this.drained()!==true)fail('STATE_CHANGED');
195
+ const second=this.inventory();if(!same(actual.map(x=>({path:x.path,mode:x.mode,hash:sha(x.bytes)})),second.map(x=>({path:x.path,mode:x.mode,hash:sha(x.bytes)}))))fail('WORKTREE_CHANGED');
196
+ const state={kind:'git-state-v1',intent_fingerprint:fingerprint,workspace_identity:identity,branch:p.branch,head_oid:head,ref_oid:ref,index_tree_oid:tree,
197
+ parent_tree_oid:indexTree(baseEntries,format),manifest_sha256:p.manifest_sha256,metadata:meta,objects_present:p.objects.filter(x=>objects.get(x.oid)).map(x=>x.oid).sort()};
198
+ return {...state,fingerprint:digest(state)};
199
+ }
200
+ preflight(intent){
201
+ const s=this.capture(intent),p=intent.plan;
202
+ if(s.head_oid!==p.parent||s.ref_oid!==p.parent||s.index_tree_oid!==s.parent_tree_oid)fail('BASELINE_CHANGED');
203
+ validateGitBaseline(intent,s);
204
+ return {intent_fingerprint:intent.fingerprint,drained:true,manifest_sha256:s.manifest_sha256,workspace_identity:s.workspace_identity,branch:p.branch,parent:p.parent,baseline:s};
205
+ }
206
+ observe(intent){
207
+ const s=this.capture(intent),p=intent.plan;
208
+ if(s.head_oid!==p.computed_commit_oid||s.ref_oid!==p.computed_commit_oid||s.index_tree_oid!==p.tree_oid||s.objects_present.length!==p.objects.length)fail('NOT_APPLIED');
209
+ return {kind:'commit-readback-v1',intent_fingerprint:intent.fingerprint,drained:true,object_exists:true,head_oid:s.head_oid,ref_oid:s.ref_oid,
210
+ commit_oid:p.computed_commit_oid,tree_oid:p.tree_oid,index_tree_oid:s.index_tree_oid,branch:p.branch,parent:p.parent,manifest_sha256:s.manifest_sha256,workspace_identity:s.workspace_identity};
211
+ }
212
+ unchanged(intent,baseline){
213
+ validateGitBaseline(intent,baseline);const {fingerprint}=baseline;
214
+ const current=this.preflight(intent).baseline;if(!same(current,baseline))fail('EFFECT_UNKNOWN');
215
+ return {kind:'commit-not-applied-v1',intent_fingerprint:intent.fingerprint,baseline_fingerprint:fingerprint,drained:true,unchanged:true,object_presence_unchanged:true};
216
+ }
217
+ }