@hunter-harness/workflow-harness 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +30 -1
  4. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +1 -0
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-execute/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/scripts/harness_archive.py +146 -8
  18. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +538 -4
  20. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  21. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-archive/reference.md +30 -1
  23. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
  24. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +146 -8
  37. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -1
  38. package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
  39. package/harness/bundles/general/codex/.harness-build.json +1 -1
  40. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-archive/reference.md +30 -1
  42. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +1 -0
  43. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/harness-execute/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/scripts/harness_archive.py +146 -8
  56. package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -1
  57. package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +538 -4
  58. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  59. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  60. package/harness/bundles/general/cursor/harness-archive/reference.md +30 -1
  61. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +1 -0
  62. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-execute/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  73. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/scripts/harness_archive.py +146 -8
  75. package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -1
  76. package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +538 -4
  77. package/harness/bundles/general/pi/.harness-build.json +6 -0
  78. package/harness/bundles/general/pi/CONTEXT.md +98 -0
  79. package/harness/bundles/general/pi/README.md +478 -0
  80. package/harness/bundles/general/pi/contracts/build-profile-v3.schema.json +302 -0
  81. package/harness/bundles/general/pi/contracts/ci-metrics.schema.json +36 -0
  82. package/harness/bundles/general/pi/contracts/doctor-capability.schema.json +43 -0
  83. package/harness/bundles/general/pi/contracts/external-platform-observation.schema.json +62 -0
  84. package/harness/bundles/general/pi/contracts/fixtures/managed-execution.json +341 -0
  85. package/harness/bundles/general/pi/contracts/process-identity.schema.json +132 -0
  86. package/harness/bundles/general/pi/contracts/run-session.schema.json +142 -0
  87. package/harness/bundles/general/pi/contracts/service-retirement-receipt.schema.json +85 -0
  88. package/harness/bundles/general/pi/contracts/service-session.schema.json +147 -0
  89. package/harness/bundles/general/pi/contracts/workflow-policy.json +574 -0
  90. package/harness/bundles/general/pi/harness-archive/SKILL.md +322 -0
  91. package/harness/bundles/general/pi/harness-archive/checklist.md +71 -0
  92. package/harness/bundles/general/pi/harness-archive/reference.md +150 -0
  93. package/harness/bundles/general/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
  94. package/harness/bundles/general/pi/harness-archive/templates/summary-data-template.json +218 -0
  95. package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +116 -0
  96. package/harness/bundles/general/pi/harness-codebase-map/checklist.md +126 -0
  97. package/harness/bundles/general/pi/harness-codebase-map/reference.md +364 -0
  98. package/harness/bundles/general/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  99. package/harness/bundles/general/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
  100. package/harness/bundles/general/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  101. package/harness/bundles/general/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  102. package/harness/bundles/general/pi/harness-codebase-map/templates/STACK.md +55 -0
  103. package/harness/bundles/general/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
  104. package/harness/bundles/general/pi/harness-codebase-map/templates/TESTING.md +64 -0
  105. package/harness/bundles/general/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  106. package/harness/bundles/general/pi/harness-codebase-map/templates/map-summary.md +64 -0
  107. package/harness/bundles/general/pi/harness-execute/SKILL.md +170 -0
  108. package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +103 -0
  109. package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +97 -0
  110. package/harness/bundles/general/pi/harness-plan/SKILL.md +158 -0
  111. package/harness/bundles/general/pi/harness-plan/checklist.md +243 -0
  112. package/harness/bundles/general/pi/harness-plan/protocols.md +133 -0
  113. package/harness/bundles/general/pi/harness-plan/reference.md +468 -0
  114. package/harness/bundles/general/pi/harness-pull/SKILL.md +55 -0
  115. package/harness/bundles/general/pi/harness-push/SKILL.md +83 -0
  116. package/harness/bundles/general/pi/harness-review/SKILL.md +175 -0
  117. package/harness/bundles/general/pi/harness-review/checklist.md +124 -0
  118. package/harness/bundles/general/pi/harness-review/protocols.md +69 -0
  119. package/harness/bundles/general/pi/harness-review/reference.md +114 -0
  120. package/harness/bundles/general/pi/harness-run/SKILL.md +188 -0
  121. package/harness/bundles/general/pi/harness-run/checklist.md +279 -0
  122. package/harness/bundles/general/pi/harness-run/protocols.md +141 -0
  123. package/harness/bundles/general/pi/harness-run/reference.md +886 -0
  124. package/harness/bundles/general/pi/harness-submit/SKILL.md +182 -0
  125. package/harness/bundles/general/pi/harness-submit/checklist.md +452 -0
  126. package/harness/bundles/general/pi/harness-submit/reference.md +152 -0
  127. package/harness/bundles/general/pi/harness-sync/SKILL.md +72 -0
  128. package/harness/bundles/general/pi/harness-sync/reference.md +176 -0
  129. package/harness/bundles/general/pi/harness-test/SKILL.md +250 -0
  130. package/harness/bundles/general/pi/harness-test/checklist.md +359 -0
  131. package/harness/bundles/general/pi/harness-test/pitfalls-java.md +14 -0
  132. package/harness/bundles/general/pi/harness-test/pitfalls.md +307 -0
  133. package/harness/bundles/general/pi/harness-test/reference.md +880 -0
  134. package/harness/bundles/general/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
  135. package/harness/bundles/general/pi/protocols/archive-report-protocol.md +196 -0
  136. package/harness/bundles/general/pi/protocols/ci-layering-protocol.md +22 -0
  137. package/harness/bundles/general/pi/protocols/evidence-based-reporting-protocol.md +134 -0
  138. package/harness/bundles/general/pi/protocols/execution-session-protocol.md +52 -0
  139. package/harness/bundles/general/pi/protocols/external-platform-protocol.md +27 -0
  140. package/harness/bundles/general/pi/protocols/ledger-protocol.md +365 -0
  141. package/harness/bundles/general/pi/protocols/memory-closeout-protocol.md +21 -0
  142. package/harness/bundles/general/pi/protocols/model-routing-protocol.md +22 -0
  143. package/harness/bundles/general/pi/protocols/powershell-protocol.md +238 -0
  144. package/harness/bundles/general/pi/protocols/registry-governance-protocol.md +31 -0
  145. package/harness/bundles/general/pi/protocols/report-pipeline-protocol.md +172 -0
  146. package/harness/bundles/general/pi/protocols/sensitive-info-protocol.md +110 -0
  147. package/harness/bundles/general/pi/protocols/state-layout-protocol.md +209 -0
  148. package/harness/bundles/general/pi/protocols/submit-protocol.md +78 -0
  149. package/harness/bundles/general/pi/protocols/verification-graph-protocol.md +32 -0
  150. package/harness/bundles/general/pi/scripts/harness_adoption_metrics.py +391 -0
  151. package/harness/bundles/general/pi/scripts/harness_apidoc.py +53 -0
  152. package/harness/bundles/general/pi/scripts/harness_archive.py +11345 -0
  153. package/harness/bundles/general/pi/scripts/harness_change.py +1626 -0
  154. package/harness/bundles/general/pi/scripts/harness_check_gate.py +113 -0
  155. package/harness/bundles/general/pi/scripts/harness_context.py +1774 -0
  156. package/harness/bundles/general/pi/scripts/harness_deploy.py +968 -0
  157. package/harness/bundles/general/pi/scripts/harness_efficiency.py +443 -0
  158. package/harness/bundles/general/pi/scripts/harness_environment.py +1690 -0
  159. package/harness/bundles/general/pi/scripts/harness_events.py +2234 -0
  160. package/harness/bundles/general/pi/scripts/harness_events_sync.py +1425 -0
  161. package/harness/bundles/general/pi/scripts/harness_execution_contracts.py +596 -0
  162. package/harness/bundles/general/pi/scripts/harness_fixback.py +1975 -0
  163. package/harness/bundles/general/pi/scripts/harness_gate.py +3816 -0
  164. package/harness/bundles/general/pi/scripts/harness_headless.py +37 -0
  165. package/harness/bundles/general/pi/scripts/harness_integration.py +2564 -0
  166. package/harness/bundles/general/pi/scripts/harness_knowledge_candidates.py +796 -0
  167. package/harness/bundles/general/pi/scripts/harness_ledger.py +3556 -0
  168. package/harness/bundles/general/pi/scripts/harness_migration_head.py +220 -0
  169. package/harness/bundles/general/pi/scripts/harness_orchestration.py +344 -0
  170. package/harness/bundles/general/pi/scripts/harness_paths.py +676 -0
  171. package/harness/bundles/general/pi/scripts/harness_phase.py +1726 -0
  172. package/harness/bundles/general/pi/scripts/harness_plan_aggregate.py +249 -0
  173. package/harness/bundles/general/pi/scripts/harness_plan_finalize.py +1016 -0
  174. package/harness/bundles/general/pi/scripts/harness_preflight.py +650 -0
  175. package/harness/bundles/general/pi/scripts/harness_process.py +1065 -0
  176. package/harness/bundles/general/pi/scripts/harness_profile.py +1817 -0
  177. package/harness/bundles/general/pi/scripts/harness_report_model.py +442 -0
  178. package/harness/bundles/general/pi/scripts/harness_retry.py +175 -0
  179. package/harness/bundles/general/pi/scripts/harness_review.py +631 -0
  180. package/harness/bundles/general/pi/scripts/harness_runtime.py +2243 -0
  181. package/harness/bundles/general/pi/scripts/harness_service.py +3355 -0
  182. package/harness/bundles/general/pi/scripts/harness_state.py +365 -0
  183. package/harness/bundles/general/pi/scripts/harness_sync.py +407 -0
  184. package/harness/bundles/general/pi/scripts/harness_test_cleanup.py +175 -0
  185. package/harness/bundles/general/pi/scripts/harness_test_guard.py +1899 -0
  186. package/harness/bundles/general/pi/scripts/harness_test_runner.py +1586 -0
  187. package/harness/bundles/general/pi/scripts/harness_verification.py +593 -0
  188. package/harness/bundles/general/pi/scripts/harness_workflow_policy.py +266 -0
  189. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  190. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  192. package/harness/bundles/java/claude-code/harness-archive/reference.md +30 -1
  193. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +1 -0
  194. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  195. package/harness/bundles/java/claude-code/harness-execute/SKILL.md +1 -1
  196. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  197. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  198. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  199. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  200. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  201. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  202. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  203. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  204. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  205. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  206. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  207. package/harness/bundles/java/claude-code/scripts/harness_archive.py +146 -8
  208. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -1
  209. package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +538 -4
  210. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  211. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  212. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  213. package/harness/bundles/java/codebuddy/harness-archive/reference.md +30 -1
  214. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
  215. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  216. package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +1 -1
  217. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  218. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  219. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  220. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  221. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  222. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  223. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  224. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  225. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  226. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  227. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  228. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +146 -8
  229. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -1
  230. package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
  231. package/harness/bundles/java/codex/.harness-build.json +1 -1
  232. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  233. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  234. package/harness/bundles/java/codex/harness-archive/reference.md +30 -1
  235. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +1 -0
  236. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  237. package/harness/bundles/java/codex/harness-execute/SKILL.md +1 -1
  238. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  239. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  240. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  241. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  242. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  243. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  244. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  245. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  246. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  247. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  248. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  249. package/harness/bundles/java/codex/scripts/harness_archive.py +146 -8
  250. package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -1
  251. package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +538 -4
  252. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  253. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  254. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  255. package/harness/bundles/java/cursor/harness-archive/reference.md +30 -1
  256. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +1 -0
  257. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  258. package/harness/bundles/java/cursor/harness-execute/SKILL.md +1 -1
  259. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  260. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  261. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  262. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  263. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  264. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  265. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  266. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  267. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  268. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  269. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  270. package/harness/bundles/java/cursor/scripts/harness_archive.py +146 -8
  271. package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -1
  272. package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +538 -4
  273. package/harness/bundles/java/pi/.harness-build.json +6 -0
  274. package/harness/bundles/java/pi/CONTEXT.md +98 -0
  275. package/harness/bundles/java/pi/README.md +478 -0
  276. package/harness/bundles/java/pi/contracts/build-profile-v3.schema.json +302 -0
  277. package/harness/bundles/java/pi/contracts/ci-metrics.schema.json +36 -0
  278. package/harness/bundles/java/pi/contracts/doctor-capability.schema.json +43 -0
  279. package/harness/bundles/java/pi/contracts/external-platform-observation.schema.json +62 -0
  280. package/harness/bundles/java/pi/contracts/fixtures/managed-execution.json +341 -0
  281. package/harness/bundles/java/pi/contracts/process-identity.schema.json +132 -0
  282. package/harness/bundles/java/pi/contracts/run-session.schema.json +142 -0
  283. package/harness/bundles/java/pi/contracts/service-retirement-receipt.schema.json +85 -0
  284. package/harness/bundles/java/pi/contracts/service-session.schema.json +147 -0
  285. package/harness/bundles/java/pi/contracts/workflow-policy.json +574 -0
  286. package/harness/bundles/java/pi/harness-apidoc/SKILL.md +97 -0
  287. package/harness/bundles/java/pi/harness-apidoc/checklist.md +142 -0
  288. package/harness/bundles/java/pi/harness-apidoc/reference.md +233 -0
  289. package/harness/bundles/java/pi/harness-archive/SKILL.md +322 -0
  290. package/harness/bundles/java/pi/harness-archive/checklist.md +71 -0
  291. package/harness/bundles/java/pi/harness-archive/reference.md +150 -0
  292. package/harness/bundles/java/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
  293. package/harness/bundles/java/pi/harness-archive/templates/summary-data-template.json +218 -0
  294. package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +116 -0
  295. package/harness/bundles/java/pi/harness-codebase-map/checklist.md +126 -0
  296. package/harness/bundles/java/pi/harness-codebase-map/reference.md +364 -0
  297. package/harness/bundles/java/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  298. package/harness/bundles/java/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
  299. package/harness/bundles/java/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  300. package/harness/bundles/java/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  301. package/harness/bundles/java/pi/harness-codebase-map/templates/STACK.md +55 -0
  302. package/harness/bundles/java/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
  303. package/harness/bundles/java/pi/harness-codebase-map/templates/TESTING.md +64 -0
  304. package/harness/bundles/java/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  305. package/harness/bundles/java/pi/harness-codebase-map/templates/map-summary.md +64 -0
  306. package/harness/bundles/java/pi/harness-execute/SKILL.md +170 -0
  307. package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +103 -0
  308. package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +97 -0
  309. package/harness/bundles/java/pi/harness-package/SKILL.md +94 -0
  310. package/harness/bundles/java/pi/harness-package/checklist.md +322 -0
  311. package/harness/bundles/java/pi/harness-package/reference.md +210 -0
  312. package/harness/bundles/java/pi/harness-plan/SKILL.md +158 -0
  313. package/harness/bundles/java/pi/harness-plan/checklist.md +243 -0
  314. package/harness/bundles/java/pi/harness-plan/protocols.md +133 -0
  315. package/harness/bundles/java/pi/harness-plan/reference.md +468 -0
  316. package/harness/bundles/java/pi/harness-pull/SKILL.md +55 -0
  317. package/harness/bundles/java/pi/harness-push/SKILL.md +83 -0
  318. package/harness/bundles/java/pi/harness-review/SKILL.md +175 -0
  319. package/harness/bundles/java/pi/harness-review/checklist.md +124 -0
  320. package/harness/bundles/java/pi/harness-review/protocols.md +69 -0
  321. package/harness/bundles/java/pi/harness-review/reference.md +114 -0
  322. package/harness/bundles/java/pi/harness-run/SKILL.md +206 -0
  323. package/harness/bundles/java/pi/harness-run/checklist.md +252 -0
  324. package/harness/bundles/java/pi/harness-run/protocols.md +141 -0
  325. package/harness/bundles/java/pi/harness-run/reference.md +820 -0
  326. package/harness/bundles/java/pi/harness-submit/SKILL.md +189 -0
  327. package/harness/bundles/java/pi/harness-submit/checklist.md +452 -0
  328. package/harness/bundles/java/pi/harness-submit/reference.md +152 -0
  329. package/harness/bundles/java/pi/harness-sync/SKILL.md +72 -0
  330. package/harness/bundles/java/pi/harness-sync/reference.md +176 -0
  331. package/harness/bundles/java/pi/harness-test/SKILL.md +264 -0
  332. package/harness/bundles/java/pi/harness-test/checklist.md +320 -0
  333. package/harness/bundles/java/pi/harness-test/pitfalls-java.md +16 -0
  334. package/harness/bundles/java/pi/harness-test/pitfalls.md +307 -0
  335. package/harness/bundles/java/pi/harness-test/reference.md +792 -0
  336. package/harness/bundles/java/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
  337. package/harness/bundles/java/pi/protocols/archive-report-protocol.md +196 -0
  338. package/harness/bundles/java/pi/protocols/ci-layering-protocol.md +22 -0
  339. package/harness/bundles/java/pi/protocols/evidence-based-reporting-protocol.md +134 -0
  340. package/harness/bundles/java/pi/protocols/execution-session-protocol.md +52 -0
  341. package/harness/bundles/java/pi/protocols/external-platform-protocol.md +27 -0
  342. package/harness/bundles/java/pi/protocols/ledger-protocol.md +365 -0
  343. package/harness/bundles/java/pi/protocols/memory-closeout-protocol.md +21 -0
  344. package/harness/bundles/java/pi/protocols/model-routing-protocol.md +22 -0
  345. package/harness/bundles/java/pi/protocols/powershell-protocol.md +238 -0
  346. package/harness/bundles/java/pi/protocols/registry-governance-protocol.md +31 -0
  347. package/harness/bundles/java/pi/protocols/report-pipeline-protocol.md +172 -0
  348. package/harness/bundles/java/pi/protocols/sensitive-info-protocol.md +110 -0
  349. package/harness/bundles/java/pi/protocols/state-layout-protocol.md +209 -0
  350. package/harness/bundles/java/pi/protocols/submit-protocol.md +78 -0
  351. package/harness/bundles/java/pi/protocols/verification-graph-protocol.md +32 -0
  352. package/harness/bundles/java/pi/scripts/harness_adoption_metrics.py +391 -0
  353. package/harness/bundles/java/pi/scripts/harness_apidoc.py +53 -0
  354. package/harness/bundles/java/pi/scripts/harness_archive.py +11345 -0
  355. package/harness/bundles/java/pi/scripts/harness_change.py +1626 -0
  356. package/harness/bundles/java/pi/scripts/harness_check_gate.py +113 -0
  357. package/harness/bundles/java/pi/scripts/harness_context.py +1774 -0
  358. package/harness/bundles/java/pi/scripts/harness_deploy.py +968 -0
  359. package/harness/bundles/java/pi/scripts/harness_efficiency.py +443 -0
  360. package/harness/bundles/java/pi/scripts/harness_environment.py +1690 -0
  361. package/harness/bundles/java/pi/scripts/harness_events.py +2234 -0
  362. package/harness/bundles/java/pi/scripts/harness_events_sync.py +1425 -0
  363. package/harness/bundles/java/pi/scripts/harness_execution_contracts.py +596 -0
  364. package/harness/bundles/java/pi/scripts/harness_fixback.py +1975 -0
  365. package/harness/bundles/java/pi/scripts/harness_gate.py +3816 -0
  366. package/harness/bundles/java/pi/scripts/harness_headless.py +37 -0
  367. package/harness/bundles/java/pi/scripts/harness_integration.py +2564 -0
  368. package/harness/bundles/java/pi/scripts/harness_knowledge_candidates.py +796 -0
  369. package/harness/bundles/java/pi/scripts/harness_ledger.py +3556 -0
  370. package/harness/bundles/java/pi/scripts/harness_migration_head.py +220 -0
  371. package/harness/bundles/java/pi/scripts/harness_orchestration.py +344 -0
  372. package/harness/bundles/java/pi/scripts/harness_paths.py +676 -0
  373. package/harness/bundles/java/pi/scripts/harness_phase.py +1726 -0
  374. package/harness/bundles/java/pi/scripts/harness_plan_aggregate.py +249 -0
  375. package/harness/bundles/java/pi/scripts/harness_plan_finalize.py +1016 -0
  376. package/harness/bundles/java/pi/scripts/harness_preflight.py +650 -0
  377. package/harness/bundles/java/pi/scripts/harness_process.py +1065 -0
  378. package/harness/bundles/java/pi/scripts/harness_profile.py +1817 -0
  379. package/harness/bundles/java/pi/scripts/harness_report_model.py +442 -0
  380. package/harness/bundles/java/pi/scripts/harness_retry.py +175 -0
  381. package/harness/bundles/java/pi/scripts/harness_review.py +631 -0
  382. package/harness/bundles/java/pi/scripts/harness_runtime.py +2243 -0
  383. package/harness/bundles/java/pi/scripts/harness_service.py +3355 -0
  384. package/harness/bundles/java/pi/scripts/harness_state.py +365 -0
  385. package/harness/bundles/java/pi/scripts/harness_sync.py +407 -0
  386. package/harness/bundles/java/pi/scripts/harness_test_cleanup.py +175 -0
  387. package/harness/bundles/java/pi/scripts/harness_test_guard.py +1899 -0
  388. package/harness/bundles/java/pi/scripts/harness_test_runner.py +1586 -0
  389. package/harness/bundles/java/pi/scripts/harness_verification.py +593 -0
  390. package/harness/bundles/java/pi/scripts/harness_workflow_policy.py +266 -0
  391. package/harness/manifests/general/claude-code.json +19 -19
  392. package/harness/manifests/general/codebuddy.json +19 -19
  393. package/harness/manifests/general/codex.json +19 -19
  394. package/harness/manifests/general/cursor.json +19 -19
  395. package/harness/manifests/general/pi.json +475 -0
  396. package/harness/manifests/java/claude-code.json +21 -21
  397. package/harness/manifests/java/codebuddy.json +21 -21
  398. package/harness/manifests/java/codex.json +21 -21
  399. package/harness/manifests/java/cursor.json +21 -21
  400. package/harness/manifests/java/pi.json +499 -0
  401. package/hunter-workflow-family.json +2 -2
  402. package/package.json +1 -1
@@ -0,0 +1,1065 @@
1
+ #!/usr/bin/env python3
2
+ """Platform process identity, spawn attestation, and owned-tree termination.
3
+
4
+ OS observations never accept expected argv/cwd/owner fields. Destructive
5
+ actions require both independently observed identity closure and a
6
+ platform-specific ownership proof.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ import datetime as dt
13
+ import hashlib
14
+ import json
15
+ import os
16
+ from pathlib import Path
17
+ import signal
18
+ import subprocess
19
+ import sys
20
+ import time
21
+ from typing import Any, Callable, Mapping, Sequence
22
+ import uuid
23
+
24
+
25
+ IDENTITY_TOLERANCE_SECONDS = 1.0
26
+ PROVENANCE = {"OBSERVED", "ATTESTED", "UNAVAILABLE"}
27
+ _WINDOWS_JOB_HANDLES: dict[str, Any] = {}
28
+
29
+
30
+ def canonical_argv_hash(argv: Sequence[str]) -> str:
31
+ payload = json.dumps(
32
+ [str(item) for item in argv],
33
+ ensure_ascii=False,
34
+ separators=(",", ":"),
35
+ ).encode("utf-8")
36
+ return "sha256:" + hashlib.sha256(payload).hexdigest()
37
+
38
+
39
+ def _token_hash(value: str) -> str:
40
+ return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
41
+
42
+
43
+ def _iso(value: dt.datetime | None) -> str | None:
44
+ if value is None:
45
+ return None
46
+ return value.astimezone(dt.timezone.utc).isoformat(timespec="microseconds")
47
+
48
+
49
+ def _parse_iso(value: Any) -> dt.datetime | None:
50
+ if not isinstance(value, str) or not value.strip():
51
+ return None
52
+ text = value.strip()
53
+ if text.endswith("Z"):
54
+ text = text[:-1] + "+00:00"
55
+ try:
56
+ parsed = dt.datetime.fromisoformat(text)
57
+ except ValueError:
58
+ return None
59
+ if parsed.tzinfo is None:
60
+ parsed = parsed.astimezone()
61
+ return parsed.astimezone(dt.timezone.utc)
62
+
63
+
64
+ def is_pid_alive(pid: int) -> bool:
65
+ if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
66
+ return False
67
+ if os.name == "nt":
68
+ try:
69
+ import ctypes
70
+ from ctypes import wintypes
71
+
72
+ kernel32 = ctypes.windll.kernel32
73
+ handle = kernel32.OpenProcess(0x1000, False, pid)
74
+ if not handle:
75
+ return False
76
+ try:
77
+ exit_code = wintypes.DWORD()
78
+ if not kernel32.GetExitCodeProcess(
79
+ handle, ctypes.byref(exit_code)
80
+ ):
81
+ return False
82
+ return int(exit_code.value) == 259
83
+ finally:
84
+ kernel32.CloseHandle(handle)
85
+ except Exception:
86
+ return False
87
+ try:
88
+ os.kill(pid, 0)
89
+ return True
90
+ except ProcessLookupError:
91
+ return False
92
+ except PermissionError:
93
+ return True
94
+ except OSError:
95
+ return False
96
+
97
+
98
+ def get_process_create_time(pid: int) -> dt.datetime | None:
99
+ if not is_pid_alive(pid):
100
+ return None
101
+ if os.name == "nt":
102
+ try:
103
+ import ctypes
104
+ from ctypes import wintypes
105
+
106
+ kernel32 = ctypes.windll.kernel32
107
+ handle = kernel32.OpenProcess(0x1000, False, pid)
108
+ if not handle:
109
+ return None
110
+ try:
111
+ creation = wintypes.FILETIME()
112
+ exit_time = wintypes.FILETIME()
113
+ kernel_time = wintypes.FILETIME()
114
+ user_time = wintypes.FILETIME()
115
+ if not kernel32.GetProcessTimes(
116
+ handle,
117
+ ctypes.byref(creation),
118
+ ctypes.byref(exit_time),
119
+ ctypes.byref(kernel_time),
120
+ ctypes.byref(user_time),
121
+ ):
122
+ return None
123
+ raw = (creation.dwHighDateTime << 32) | creation.dwLowDateTime
124
+ seconds = (raw - 116444736000000000) / 10_000_000
125
+ return dt.datetime.fromtimestamp(
126
+ seconds, tz=dt.timezone.utc
127
+ )
128
+ finally:
129
+ kernel32.CloseHandle(handle)
130
+ except Exception:
131
+ return None
132
+ stat = _read_proc_stat(pid)
133
+ if stat is None:
134
+ return None
135
+ boot = _linux_boot_time()
136
+ if boot is None:
137
+ return None
138
+ try:
139
+ ticks = int(os.sysconf("SC_CLK_TCK"))
140
+ return dt.datetime.fromtimestamp(
141
+ boot + int(stat["startTicks"]) / ticks,
142
+ tz=dt.timezone.utc,
143
+ )
144
+ except (OSError, TypeError, ValueError):
145
+ return None
146
+
147
+
148
+ def get_process_executable(pid: int) -> str | None:
149
+ if not is_pid_alive(pid):
150
+ return None
151
+ if os.name == "nt":
152
+ try:
153
+ import ctypes
154
+ from ctypes import wintypes
155
+
156
+ kernel32 = ctypes.windll.kernel32
157
+ handle = kernel32.OpenProcess(0x1000, False, pid)
158
+ if not handle:
159
+ return None
160
+ try:
161
+ capacity = wintypes.DWORD(32768)
162
+ buffer = ctypes.create_unicode_buffer(capacity.value)
163
+ if not kernel32.QueryFullProcessImageNameW(
164
+ handle, 0, buffer, ctypes.byref(capacity)
165
+ ):
166
+ return None
167
+ return str(Path(buffer.value).resolve())
168
+ finally:
169
+ kernel32.CloseHandle(handle)
170
+ except Exception:
171
+ return None
172
+ try:
173
+ return str(Path(f"/proc/{pid}/exe").resolve(strict=True))
174
+ except OSError:
175
+ return None
176
+
177
+
178
+ def _linux_boot_time() -> float | None:
179
+ try:
180
+ for line in Path("/proc/stat").read_text(encoding="utf-8").splitlines():
181
+ if line.startswith("btime "):
182
+ return float(line.split()[1])
183
+ except (OSError, ValueError):
184
+ return None
185
+ return None
186
+
187
+
188
+ def _read_proc_stat(pid: int) -> dict[str, int] | None:
189
+ try:
190
+ text = Path(f"/proc/{pid}/stat").read_text(
191
+ encoding="utf-8", errors="replace"
192
+ )
193
+ right = text.rfind(")")
194
+ if right < 0:
195
+ return None
196
+ fields = text[right + 2 :].split()
197
+ return {
198
+ "parentPid": int(fields[1]),
199
+ "groupId": int(fields[2]),
200
+ "sessionId": int(fields[3]),
201
+ "startTicks": int(fields[19]),
202
+ }
203
+ except (OSError, IndexError, ValueError):
204
+ return None
205
+
206
+
207
+ def _proc_argv(pid: int) -> list[str] | None:
208
+ try:
209
+ raw = Path(f"/proc/{pid}/cmdline").read_bytes()
210
+ except OSError:
211
+ return None
212
+ if not raw:
213
+ return None
214
+ try:
215
+ return [
216
+ part.decode("utf-8", errors="strict")
217
+ for part in raw.rstrip(b"\0").split(b"\0")
218
+ ]
219
+ except UnicodeDecodeError:
220
+ return None
221
+
222
+
223
+ def _proc_cwd(pid: int) -> str | None:
224
+ try:
225
+ return str(Path(f"/proc/{pid}/cwd").resolve(strict=True))
226
+ except OSError:
227
+ return None
228
+
229
+
230
+ def _enumerate_posix_members(
231
+ group_id: int,
232
+ session_id: int,
233
+ ) -> tuple[list[int], bool]:
234
+ proc = Path("/proc")
235
+ if not proc.is_dir():
236
+ return [], False
237
+ members: list[int] = []
238
+ complete = True
239
+ for candidate in proc.iterdir():
240
+ if not candidate.name.isdigit():
241
+ continue
242
+ stat = _read_proc_stat(int(candidate.name))
243
+ if stat is None:
244
+ continue
245
+ if (
246
+ stat["groupId"] == group_id
247
+ and stat["sessionId"] == session_id
248
+ ):
249
+ members.append(int(candidate.name))
250
+ return sorted(members), complete
251
+
252
+
253
+ def normalize_process_observation(raw: Mapping[str, Any]) -> dict[str, Any]:
254
+ pid = int(raw.get("pid") or 0)
255
+ alive = bool(raw.get("alive"))
256
+ created = raw.get("createdAt")
257
+ executable = raw.get("executable")
258
+ argv_hash = raw.get("argvHash")
259
+ cwd = raw.get("workingDirectory")
260
+ parent = raw.get("parentIdentity")
261
+ tree = raw.get("treeIdentity")
262
+ return {
263
+ "schemaVersion": 1,
264
+ "pid": pid,
265
+ "alive": alive,
266
+ "createdAt": created if isinstance(created, str) else None,
267
+ "executable": executable if isinstance(executable, str) else None,
268
+ "argvHash": argv_hash if isinstance(argv_hash, str) else None,
269
+ "workingDirectory": cwd if isinstance(cwd, str) else None,
270
+ "parentIdentity": parent if isinstance(parent, dict) else None,
271
+ "ownerTokenHash": (
272
+ str(raw["ownerTokenHash"])
273
+ if isinstance(raw.get("ownerTokenHash"), str)
274
+ else None
275
+ ),
276
+ "treeIdentity": tree if isinstance(tree, dict) else None,
277
+ "fieldProvenance": {
278
+ "pid": "OBSERVED",
279
+ "createdAt": "OBSERVED" if isinstance(created, str) else "UNAVAILABLE",
280
+ "executable": (
281
+ "OBSERVED" if isinstance(executable, str) else "UNAVAILABLE"
282
+ ),
283
+ "argvHash": "OBSERVED" if isinstance(argv_hash, str) else "UNAVAILABLE",
284
+ "workingDirectory": "OBSERVED" if isinstance(cwd, str) else "UNAVAILABLE",
285
+ "parentIdentity": "OBSERVED" if isinstance(parent, dict) else "UNAVAILABLE",
286
+ "ownerTokenHash": (
287
+ "ATTESTED"
288
+ if isinstance(raw.get("ownerTokenHash"), str)
289
+ else "UNAVAILABLE"
290
+ ),
291
+ "treeIdentity": "OBSERVED" if isinstance(tree, dict) else "UNAVAILABLE",
292
+ },
293
+ "capabilities": {
294
+ "canObserveCreateTime": isinstance(created, str),
295
+ "canObserveExecutable": isinstance(executable, str),
296
+ "canObserveArgv": isinstance(argv_hash, str),
297
+ "canObserveWorkingDirectory": isinstance(cwd, str),
298
+ "canObserveParent": isinstance(parent, dict),
299
+ "canEnumerateTree": bool(
300
+ isinstance(tree, dict) and tree.get("complete") is True
301
+ ),
302
+ "canVerifyOwnership": bool(raw.get("canVerifyOwnership")),
303
+ },
304
+ }
305
+
306
+
307
+ def observe_process_identity(pid: int) -> dict[str, Any]:
308
+ """Observe OS facts only. Expected identity is intentionally not accepted."""
309
+
310
+ alive = is_pid_alive(pid)
311
+ if not alive:
312
+ return normalize_process_observation(
313
+ {"pid": pid, "alive": False, "platform": _platform_name()}
314
+ )
315
+ created = _iso(get_process_create_time(pid))
316
+ executable = get_process_executable(pid)
317
+ parent: dict[str, Any] | None = None
318
+ tree: dict[str, Any] | None = None
319
+ argv_hash: str | None = None
320
+ cwd: str | None = None
321
+ if os.name != "nt":
322
+ stat = _read_proc_stat(pid)
323
+ argv = _proc_argv(pid)
324
+ argv_hash = canonical_argv_hash(argv) if argv is not None else None
325
+ cwd = _proc_cwd(pid)
326
+ if stat is not None and stat["parentPid"] > 0:
327
+ parent = {
328
+ "pid": stat["parentPid"],
329
+ "createdAt": _iso(
330
+ get_process_create_time(stat["parentPid"])
331
+ ),
332
+ "executable": get_process_executable(stat["parentPid"]),
333
+ }
334
+ members, complete = _enumerate_posix_members(
335
+ stat["groupId"], stat["sessionId"]
336
+ )
337
+ tree = {
338
+ "platform": "LINUX" if sys.platform.startswith("linux") else "POSIX",
339
+ "proofKind": "OBSERVED_SESSION",
340
+ "memberPids": members,
341
+ "complete": complete,
342
+ "groupId": stat["groupId"],
343
+ "sessionId": stat["sessionId"],
344
+ }
345
+ return normalize_process_observation(
346
+ {
347
+ "pid": pid,
348
+ "alive": True,
349
+ "createdAt": created,
350
+ "executable": executable,
351
+ "argvHash": argv_hash,
352
+ "workingDirectory": cwd,
353
+ "parentIdentity": parent,
354
+ "treeIdentity": tree,
355
+ "canVerifyOwnership": False,
356
+ }
357
+ )
358
+
359
+
360
+ def _platform_name() -> str:
361
+ if os.name == "nt":
362
+ return "WINDOWS"
363
+ return "LINUX" if sys.platform.startswith("linux") else "POSIX"
364
+
365
+
366
+ def _normalized_path(value: str) -> str:
367
+ normalized = os.path.normcase(os.path.abspath(value))
368
+ return normalized.replace("\\", "/")
369
+
370
+
371
+ def _created_at_matches(expected: Any, actual: Any) -> bool | None:
372
+ left = _parse_iso(expected)
373
+ right = _parse_iso(actual)
374
+ if left is None or right is None:
375
+ return None
376
+ return abs((left - right).total_seconds()) <= IDENTITY_TOLERANCE_SECONDS
377
+
378
+
379
+ def verify_process_identity(
380
+ expected_attestation: Mapping[str, Any],
381
+ observed_identity: Mapping[str, Any],
382
+ ) -> dict[str, Any]:
383
+ """Compare spawn attestation to independent OS observation."""
384
+
385
+ if observed_identity.get("alive") is not True:
386
+ return _decision(False, "PROCESS_IDENTITY_MISMATCH", ["process-dead"])
387
+ if expected_attestation.get("pid") != observed_identity.get("pid"):
388
+ return _decision(False, "PROCESS_IDENTITY_MISMATCH", ["pid"])
389
+ checks = (
390
+ ("createdAt", "PROCESS_CREATE_TIME_MISMATCH", _created_at_matches),
391
+ (
392
+ "executable",
393
+ "PROCESS_EXECUTABLE_MISMATCH",
394
+ lambda left, right: (
395
+ _normalized_path(left) == _normalized_path(right)
396
+ if isinstance(left, str) and isinstance(right, str)
397
+ else None
398
+ ),
399
+ ),
400
+ (
401
+ "argvHash",
402
+ "PROCESS_ARGV_MISMATCH",
403
+ lambda left, right: left == right
404
+ if isinstance(left, str) and isinstance(right, str)
405
+ else None,
406
+ ),
407
+ (
408
+ "workingDirectory",
409
+ "PROCESS_CWD_MISMATCH",
410
+ lambda left, right: (
411
+ _normalized_path(left) == _normalized_path(right)
412
+ if isinstance(left, str) and isinstance(right, str)
413
+ else None
414
+ ),
415
+ ),
416
+ (
417
+ "ownerTokenHash",
418
+ "PROCESS_OWNER_MISMATCH",
419
+ lambda left, right: (
420
+ left == right
421
+ if isinstance(left, str) and isinstance(right, str)
422
+ else True if right is None else None
423
+ ),
424
+ ),
425
+ )
426
+ unavailable: list[str] = []
427
+ expected_provenance = expected_attestation.get("fieldProvenance")
428
+ if not isinstance(expected_provenance, Mapping):
429
+ expected_provenance = {}
430
+ observed_provenance = observed_identity.get("fieldProvenance")
431
+ if not isinstance(observed_provenance, Mapping):
432
+ observed_provenance = {}
433
+ for field, reason, comparator in checks:
434
+ expected = expected_attestation.get(field)
435
+ if expected is None:
436
+ continue
437
+ actual = observed_identity.get(field)
438
+ expected_source = expected_provenance.get(field)
439
+ observed_source = observed_provenance.get(field)
440
+ # Create time, executable and owner/parent facts must come from an
441
+ # independent OS observation. argv/cwd may remain spawn-attested on
442
+ # platforms that cannot read them back, but a value labelled
443
+ # UNAVAILABLE is never allowed to masquerade as an observation.
444
+ independent_field = field in {
445
+ "createdAt",
446
+ "executable",
447
+ "parentIdentity",
448
+ }
449
+ if actual is not None and observed_source not in {"OBSERVED", "ATTESTED"}:
450
+ unavailable.append(field)
451
+ continue
452
+ if actual is not None and independent_field and observed_source != "OBSERVED":
453
+ unavailable.append(field)
454
+ continue
455
+ compared = comparator(expected, actual)
456
+ if compared is False:
457
+ return _decision(False, reason, [field])
458
+ if compared is None:
459
+ if field in {"argvHash", "workingDirectory"} and expected_source == "ATTESTED" and actual is None:
460
+ # The attestation is the only trustworthy source on a
461
+ # platform without argv/cwd observation. Core identity and
462
+ # ownership proof checks below still have to close.
463
+ continue
464
+ unavailable.append(field)
465
+ expected_parent = expected_attestation.get("parentIdentity")
466
+ if isinstance(expected_parent, dict):
467
+ actual_parent = observed_identity.get("parentIdentity")
468
+ if not isinstance(actual_parent, dict):
469
+ unavailable.append("parentIdentity")
470
+ elif observed_provenance.get("parentIdentity") != "OBSERVED":
471
+ unavailable.append("parentIdentity")
472
+ elif expected_parent.get("pid") != actual_parent.get("pid"):
473
+ return _decision(
474
+ False, "PROCESS_PARENT_MISMATCH", ["parentIdentity.pid"]
475
+ )
476
+ else:
477
+ parent_time = _created_at_matches(
478
+ expected_parent.get("createdAt"),
479
+ actual_parent.get("createdAt"),
480
+ )
481
+ if parent_time is False:
482
+ return _decision(
483
+ False,
484
+ "PROCESS_PARENT_MISMATCH",
485
+ ["parentIdentity.createdAt"],
486
+ )
487
+ if parent_time is None:
488
+ unavailable.append("parentIdentity.createdAt")
489
+ if unavailable:
490
+ return _decision(False, "IDENTITY_UNVERIFIABLE", unavailable)
491
+ return _decision(True, "PROCESS_IDENTITY_VERIFIED", [])
492
+
493
+
494
+ def _decision(ok: bool, reason: str, details: list[str]) -> dict[str, Any]:
495
+ return {
496
+ "ok": ok,
497
+ "authorized": ok,
498
+ "reasonCode": reason,
499
+ "details": details,
500
+ }
501
+
502
+
503
+ def validate_ownership_proof(
504
+ expected_attestation: Mapping[str, Any],
505
+ ownership_proof: Mapping[str, Any] | None,
506
+ observed_members: Sequence[Mapping[str, Any]],
507
+ ) -> dict[str, Any]:
508
+ if not isinstance(ownership_proof, Mapping):
509
+ return _decision(False, "IDENTITY_UNVERIFIABLE", ["proof-missing"])
510
+ if (
511
+ expected_attestation.get("ownerTokenHash") is None
512
+ or ownership_proof.get("ownershipTokenHash")
513
+ != expected_attestation.get("ownerTokenHash")
514
+ ):
515
+ return _decision(False, "PROCESS_OWNER_MISMATCH", ["ownerTokenHash"])
516
+ if ownership_proof.get("membersComplete") is not True:
517
+ return _decision(False, "IDENTITY_UNVERIFIABLE", ["members-incomplete"])
518
+ if ownership_proof.get("leaderExited") is True:
519
+ return _decision(False, "IDENTITY_UNVERIFIABLE", ["leader-exited"])
520
+ kind = ownership_proof.get("kind")
521
+ if kind not in {"WINDOWS_NAMED_JOB", "LINUX_PIDFD_SESSION", "POSIX_MEMBERS"}:
522
+ return _decision(False, "IDENTITY_UNVERIFIABLE", ["proof-kind"])
523
+ expected_members = ownership_proof.get("members")
524
+ if not isinstance(expected_members, list) or not expected_members:
525
+ return _decision(False, "IDENTITY_UNVERIFIABLE", ["attested-members"])
526
+ expected_by_pid = {
527
+ item.get("pid"): item
528
+ for item in expected_members
529
+ if isinstance(item, Mapping) and isinstance(item.get("pid"), int)
530
+ }
531
+ observed_by_pid = {
532
+ item.get("pid"): item
533
+ for item in observed_members
534
+ if isinstance(item, Mapping) and item.get("alive") is True
535
+ }
536
+ if set(expected_by_pid) != set(observed_by_pid):
537
+ return _decision(
538
+ False,
539
+ "IDENTITY_UNVERIFIABLE",
540
+ ["foreign-or-missing-member"],
541
+ )
542
+ leader_pid = ownership_proof.get("leaderPid")
543
+ if leader_pid not in observed_by_pid:
544
+ return _decision(False, "IDENTITY_UNVERIFIABLE", ["leader-missing"])
545
+ for pid, expected in expected_by_pid.items():
546
+ observed = observed_by_pid[pid]
547
+ verified = verify_process_identity(expected, observed)
548
+ if not verified["ok"]:
549
+ return verified
550
+ expected_tree = expected.get("treeIdentity")
551
+ observed_tree = observed.get("treeIdentity")
552
+ if kind != "WINDOWS_NAMED_JOB":
553
+ if not isinstance(observed_tree, Mapping):
554
+ return _decision(
555
+ False, "IDENTITY_UNVERIFIABLE", ["tree-unavailable"]
556
+ )
557
+ for field in ("groupId", "sessionId"):
558
+ proof_value = ownership_proof.get(field)
559
+ if proof_value is not None and observed_tree.get(field) != proof_value:
560
+ return _decision(
561
+ False,
562
+ "IDENTITY_UNVERIFIABLE",
563
+ [f"{field}-reused"],
564
+ )
565
+ return {
566
+ "ok": True,
567
+ "authorized": True,
568
+ "reasonCode": "OWNERSHIP_PROOF_VERIFIED",
569
+ "details": [],
570
+ "memberPids": sorted(int(pid) for pid in observed_by_pid),
571
+ }
572
+
573
+
574
+ def _observe_proof_members(
575
+ ownership_proof: Mapping[str, Any] | None,
576
+ ) -> list[dict[str, Any]]:
577
+ if not isinstance(ownership_proof, Mapping):
578
+ return []
579
+ members = ownership_proof.get("members")
580
+ if not isinstance(members, list):
581
+ return []
582
+ if ownership_proof.get("kind") == "WINDOWS_NAMED_JOB":
583
+ pids = _windows_job_member_pids(ownership_proof)
584
+ if pids is None:
585
+ if isinstance(ownership_proof, dict):
586
+ ownership_proof["membersComplete"] = False
587
+ return []
588
+ return [observe_process_identity(pid) for pid in pids]
589
+ observed = [
590
+ observe_process_identity(int(item["pid"]))
591
+ for item in members
592
+ if isinstance(item, Mapping) and isinstance(item.get("pid"), int)
593
+ ]
594
+ if ownership_proof.get("kind") != "WINDOWS_NAMED_JOB":
595
+ group_id = ownership_proof.get("groupId")
596
+ session_id = ownership_proof.get("sessionId")
597
+ if isinstance(group_id, int) and isinstance(session_id, int):
598
+ pids, complete = _enumerate_posix_members(group_id, session_id)
599
+ ownership_proof["membersComplete"] = bool(complete)
600
+ known = {item["pid"] for item in observed}
601
+ observed.extend(
602
+ observe_process_identity(pid) for pid in pids if pid not in known
603
+ )
604
+ return observed
605
+
606
+
607
+ def terminate_owned_tree(
608
+ expected_attestation: Mapping[str, Any],
609
+ ownership_proof: Mapping[str, Any] | None,
610
+ timeout_policy: Mapping[str, Any] | None = None,
611
+ *,
612
+ member_observer: Callable[
613
+ [Mapping[str, Any] | None], Sequence[Mapping[str, Any]]
614
+ ] = _observe_proof_members,
615
+ signaler: Callable[[int, int], None] = os.kill,
616
+ ) -> dict[str, Any]:
617
+ """Terminate only after proof and every current member identity close."""
618
+
619
+ observed = list(member_observer(ownership_proof))
620
+ proof_result = validate_ownership_proof(
621
+ expected_attestation, ownership_proof, observed
622
+ )
623
+ if not proof_result["ok"]:
624
+ return {
625
+ **proof_result,
626
+ "terminatedPids": [],
627
+ "cleanupComplete": False,
628
+ }
629
+ assert ownership_proof is not None
630
+ pids = list(reversed(proof_result["memberPids"]))
631
+ terminated: list[int] = []
632
+ if ownership_proof.get("kind") == "WINDOWS_NAMED_JOB":
633
+ proof_id = str(ownership_proof.get("proofId") or "")
634
+ job = _WINDOWS_JOB_HANDLES.get(proof_id)
635
+ if job is None:
636
+ job = _WindowsNamedJob(
637
+ str(ownership_proof.get("jobName") or ""),
638
+ configure=False,
639
+ )
640
+ if job is None or not job.terminate():
641
+ return {
642
+ "ok": False,
643
+ "authorized": False,
644
+ "reasonCode": "IDENTITY_UNVERIFIABLE",
645
+ "details": ["named-job-unavailable"],
646
+ "terminatedPids": [],
647
+ "cleanupComplete": False,
648
+ }
649
+ terminated.extend(pids)
650
+ else:
651
+ for pid in pids:
652
+ try:
653
+ signaler(pid, signal.SIGTERM)
654
+ terminated.append(pid)
655
+ except ProcessLookupError:
656
+ terminated.append(pid)
657
+ except OSError as exc:
658
+ return {
659
+ "ok": False,
660
+ "authorized": True,
661
+ "reasonCode": "SERVICE_STOP_FAILED",
662
+ "details": [str(exc)],
663
+ "terminatedPids": terminated,
664
+ "cleanupComplete": False,
665
+ }
666
+ grace = float((timeout_policy or {}).get("graceSeconds", 3.0))
667
+ deadline = time.monotonic() + max(0.0, grace)
668
+ remaining = [
669
+ item
670
+ for item in member_observer(ownership_proof)
671
+ if item.get("alive") is True
672
+ ]
673
+ while remaining and time.monotonic() < deadline:
674
+ time.sleep(min(0.05, max(0.0, deadline - time.monotonic())))
675
+ remaining = [
676
+ item
677
+ for item in member_observer(ownership_proof)
678
+ if item.get("alive") is True
679
+ ]
680
+ if remaining and ownership_proof.get("kind") != "WINDOWS_NAMED_JOB":
681
+ for item in remaining:
682
+ try:
683
+ signaler(int(item["pid"]), signal.SIGKILL)
684
+ except OSError:
685
+ pass
686
+ remaining = [
687
+ item
688
+ for item in member_observer(ownership_proof)
689
+ if item.get("alive") is True
690
+ ]
691
+ return {
692
+ "ok": not remaining,
693
+ "authorized": True,
694
+ "reasonCode": (
695
+ "SERVICE_STOPPED" if not remaining else "SERVICE_STOP_TIMEOUT"
696
+ ),
697
+ "details": [],
698
+ "terminatedPids": sorted(set(terminated)),
699
+ "cleanupComplete": not remaining,
700
+ }
701
+
702
+
703
+ class _WindowsNamedJob:
704
+ def __init__(self, name: str, *, configure: bool = True) -> None:
705
+ self.name = name
706
+ self.handle: Any = None
707
+ if os.name != "nt":
708
+ return
709
+ try:
710
+ import ctypes
711
+
712
+ kernel32 = ctypes.windll.kernel32
713
+ kernel32.CreateJobObjectW.restype = ctypes.c_void_p
714
+ handle = kernel32.CreateJobObjectW(None, name)
715
+ self.handle = int(handle) if handle else None
716
+ if self.handle is not None and configure:
717
+ self._configure_kill_on_close()
718
+ except Exception:
719
+ if self.handle is not None:
720
+ try:
721
+ ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
722
+ except Exception:
723
+ pass
724
+ self.handle = None
725
+
726
+ def _configure_kill_on_close(self) -> None:
727
+ if self.handle is None:
728
+ return
729
+ import ctypes
730
+ from ctypes import wintypes
731
+
732
+ class BasicLimit(ctypes.Structure):
733
+ _fields_ = [
734
+ ("PerProcessUserTimeLimit", ctypes.c_int64),
735
+ ("PerJobUserTimeLimit", ctypes.c_int64),
736
+ ("LimitFlags", wintypes.DWORD),
737
+ ("MinimumWorkingSetSize", ctypes.c_size_t),
738
+ ("MaximumWorkingSetSize", ctypes.c_size_t),
739
+ ("ActiveProcessLimit", wintypes.DWORD),
740
+ ("Affinity", ctypes.c_size_t),
741
+ ("PriorityClass", wintypes.DWORD),
742
+ ("SchedulingClass", wintypes.DWORD),
743
+ ]
744
+
745
+ class IoCounters(ctypes.Structure):
746
+ _fields_ = [
747
+ ("ReadOperationCount", ctypes.c_uint64),
748
+ ("WriteOperationCount", ctypes.c_uint64),
749
+ ("OtherOperationCount", ctypes.c_uint64),
750
+ ("ReadTransferCount", ctypes.c_uint64),
751
+ ("WriteTransferCount", ctypes.c_uint64),
752
+ ("OtherTransferCount", ctypes.c_uint64),
753
+ ]
754
+
755
+ class ExtendedLimit(ctypes.Structure):
756
+ _fields_ = [
757
+ ("BasicLimitInformation", BasicLimit),
758
+ ("IoInfo", IoCounters),
759
+ ("ProcessMemoryLimit", ctypes.c_size_t),
760
+ ("JobMemoryLimit", ctypes.c_size_t),
761
+ ("PeakProcessMemoryUsed", ctypes.c_size_t),
762
+ ("PeakJobMemoryUsed", ctypes.c_size_t),
763
+ ]
764
+
765
+ info = ExtendedLimit()
766
+ info.BasicLimitInformation.LimitFlags = 0x00002000
767
+ if not ctypes.windll.kernel32.SetInformationJobObject(
768
+ ctypes.c_void_p(self.handle),
769
+ 9,
770
+ ctypes.byref(info),
771
+ ctypes.sizeof(info),
772
+ ):
773
+ raise OSError("SetInformationJobObject(KILL_ON_JOB_CLOSE) failed")
774
+
775
+ def assign(self, process: subprocess.Popen[Any]) -> bool:
776
+ if self.handle is None:
777
+ return False
778
+ try:
779
+ import ctypes
780
+
781
+ return bool(
782
+ ctypes.windll.kernel32.AssignProcessToJobObject(
783
+ ctypes.c_void_p(self.handle),
784
+ ctypes.c_void_p(int(process._handle)), # type: ignore[attr-defined]
785
+ )
786
+ )
787
+ except Exception:
788
+ return False
789
+
790
+ def terminate(self) -> bool:
791
+ opened = False
792
+ try:
793
+ import ctypes
794
+
795
+ if self.handle is None and os.name == "nt" and self.name:
796
+ kernel32 = ctypes.windll.kernel32
797
+ kernel32.OpenJobObjectW.restype = ctypes.c_void_p
798
+ raw = kernel32.OpenJobObjectW(0x0001, False, self.name)
799
+ if raw:
800
+ self.handle = int(raw)
801
+ opened = True
802
+ if self.handle is None:
803
+ return False
804
+ return bool(
805
+ ctypes.windll.kernel32.TerminateJobObject(
806
+ ctypes.c_void_p(self.handle), 1
807
+ )
808
+ )
809
+ except Exception:
810
+ return False
811
+ finally:
812
+ if opened and self.handle is not None:
813
+ try:
814
+ ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
815
+ except Exception:
816
+ pass
817
+ self.handle = None
818
+
819
+ def close(self) -> None:
820
+ if self.handle is None:
821
+ return
822
+ try:
823
+ import ctypes
824
+
825
+ ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
826
+ finally:
827
+ self.handle = None
828
+
829
+
830
+ def _windows_job_member_pids(
831
+ ownership_proof: Mapping[str, Any],
832
+ ) -> list[int] | None:
833
+ if os.name != "nt":
834
+ return None
835
+ proof_id = str(ownership_proof.get("proofId") or "")
836
+ job = _WINDOWS_JOB_HANDLES.get(proof_id)
837
+ handle = job.handle if job is not None else None
838
+ opened = False
839
+ try:
840
+ import ctypes
841
+
842
+ kernel32 = ctypes.windll.kernel32
843
+ if handle is None:
844
+ name = ownership_proof.get("jobName")
845
+ if not isinstance(name, str) or not name:
846
+ return None
847
+ kernel32.OpenJobObjectW.restype = ctypes.c_void_p
848
+ raw = kernel32.OpenJobObjectW(0x0004 | 0x0008, False, name)
849
+ if not raw:
850
+ return None
851
+ handle = int(raw)
852
+ opened = True
853
+ pointer_size = ctypes.sizeof(ctypes.c_size_t)
854
+ capacity = 4096
855
+ buffer = ctypes.create_string_buffer(8 + pointer_size * capacity)
856
+ returned = ctypes.c_uint32()
857
+ if not kernel32.QueryInformationJobObject(
858
+ ctypes.c_void_p(handle),
859
+ 3,
860
+ buffer,
861
+ ctypes.sizeof(buffer),
862
+ ctypes.byref(returned),
863
+ ):
864
+ return None
865
+ assigned = ctypes.c_uint32.from_buffer_copy(buffer.raw[0:4]).value
866
+ count = ctypes.c_uint32.from_buffer_copy(buffer.raw[4:8]).value
867
+ if count > assigned or count > capacity:
868
+ return None
869
+ array_type = ctypes.c_size_t * count
870
+ members = array_type.from_buffer_copy(
871
+ buffer.raw[8 : 8 + pointer_size * count]
872
+ )
873
+ return sorted(int(pid) for pid in members)
874
+ except Exception:
875
+ return None
876
+ finally:
877
+ if opened and handle is not None:
878
+ try:
879
+ import ctypes
880
+
881
+ ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(handle))
882
+ except Exception:
883
+ pass
884
+
885
+
886
+ @dataclass
887
+ class SpawnedProcess:
888
+ process: subprocess.Popen[Any]
889
+ attestation: dict[str, Any]
890
+ observedIdentity: dict[str, Any]
891
+ ownershipProof: dict[str, Any] | None
892
+ _job: _WindowsNamedJob | None = None
893
+ _pidfd: int | None = None
894
+
895
+ def close(self) -> None:
896
+ if self._pidfd is not None:
897
+ try:
898
+ os.close(self._pidfd)
899
+ except OSError:
900
+ pass
901
+ self._pidfd = None
902
+ if self._job is not None:
903
+ proof_id = (
904
+ str(self.ownershipProof.get("proofId") or "")
905
+ if isinstance(self.ownershipProof, dict)
906
+ else ""
907
+ )
908
+ if proof_id:
909
+ _WINDOWS_JOB_HANDLES.pop(proof_id, None)
910
+ # KILL_ON_JOB_CLOSE is the durable backstop, but explicitly
911
+ # terminating the owned job first closes a short race where a
912
+ # descendant is still being attached while the leader exits.
913
+ try:
914
+ self._job.terminate()
915
+ except Exception:
916
+ pass
917
+ self._job.close()
918
+ self._job = None
919
+
920
+
921
+ def capture_owned_members(spawned: SpawnedProcess) -> dict[str, Any] | None:
922
+ """Refresh a spawn proof with the provider's current owned members.
923
+
924
+ A process can create children between spawn and cleanup (notably console
925
+ hosts on Windows). The provider, rather than each consumer, must capture
926
+ that membership before identity verification and termination.
927
+ """
928
+
929
+ proof = spawned.ownershipProof
930
+ leader = spawned.attestation
931
+ if not isinstance(proof, dict) or not isinstance(leader, dict):
932
+ return proof if isinstance(proof, dict) else None
933
+ observed = list(_observe_proof_members(proof))
934
+ if not observed:
935
+ return proof
936
+ leader_pid = leader.get("pid")
937
+ members: list[dict[str, Any]] = []
938
+ for item in observed:
939
+ if item.get("pid") == leader_pid:
940
+ members.append(json.loads(json.dumps(leader)))
941
+ else:
942
+ members.append(json.loads(json.dumps(item)))
943
+ if leader_pid in {item.get("pid") for item in members}:
944
+ proof["members"] = members
945
+ proof["membersComplete"] = True
946
+ return proof
947
+
948
+
949
+ def spawn_structured_argv(
950
+ argv: Sequence[str],
951
+ *,
952
+ cwd: Path | str,
953
+ environment: Mapping[str, str] | None,
954
+ owner_token: str,
955
+ stdout: Any = None,
956
+ stderr: Any = None,
957
+ ) -> SpawnedProcess:
958
+ """Spawn with shell=False and return separate attestation/observation/proof."""
959
+
960
+ if (
961
+ not argv
962
+ or any(not isinstance(item, str) or "\0" in item for item in argv)
963
+ or not isinstance(owner_token, str)
964
+ or not owner_token
965
+ ):
966
+ raise ValueError("ARGUMENT_INVALID: argv and owner token are required")
967
+ root = Path(cwd).expanduser().resolve()
968
+ child_env = os.environ.copy()
969
+ child_env.update({str(key): str(value) for key, value in (environment or {}).items()})
970
+ child_env.setdefault("PYTHONUTF8", "1")
971
+ child_env.setdefault("PYTHONIOENCODING", "utf-8")
972
+ kwargs: dict[str, Any] = {
973
+ "cwd": str(root),
974
+ "env": child_env,
975
+ "shell": False,
976
+ "stdout": stdout,
977
+ "stderr": stderr,
978
+ }
979
+ if os.name == "nt":
980
+ kwargs["creationflags"] = getattr(
981
+ subprocess, "CREATE_NEW_PROCESS_GROUP", 0
982
+ )
983
+ else:
984
+ kwargs["start_new_session"] = True
985
+ process = subprocess.Popen(list(argv), **kwargs)
986
+ observed = observe_process_identity(process.pid)
987
+ owner_hash = _token_hash(owner_token)
988
+ attestation = json.loads(json.dumps(observed))
989
+ attestation["argvHash"] = canonical_argv_hash(argv)
990
+ attestation["workingDirectory"] = str(root)
991
+ attestation["ownerTokenHash"] = owner_hash
992
+ attestation["fieldProvenance"]["argvHash"] = (
993
+ "OBSERVED" if observed.get("argvHash") is not None else "ATTESTED"
994
+ )
995
+ attestation["fieldProvenance"]["workingDirectory"] = (
996
+ "OBSERVED"
997
+ if observed.get("workingDirectory") is not None
998
+ else "ATTESTED"
999
+ )
1000
+ attestation["fieldProvenance"]["ownerTokenHash"] = "ATTESTED"
1001
+ proof_id = "proof-" + uuid.uuid4().hex
1002
+ job: _WindowsNamedJob | None = None
1003
+ pidfd: int | None = None
1004
+ proof: dict[str, Any] | None
1005
+ if os.name == "nt":
1006
+ job = _WindowsNamedJob(
1007
+ "Local\\HunterHarness-" + owner_hash.split(":", 1)[1][:16] + "-" + proof_id
1008
+ )
1009
+ if job.handle is not None and job.assign(process):
1010
+ _WINDOWS_JOB_HANDLES[proof_id] = job
1011
+ proof = {
1012
+ "schemaVersion": 1,
1013
+ "proofId": proof_id,
1014
+ "kind": "WINDOWS_NAMED_JOB",
1015
+ "jobName": job.name,
1016
+ "ownershipTokenHash": owner_hash,
1017
+ "leaderPid": process.pid,
1018
+ "leaderCreatedAt": attestation.get("createdAt"),
1019
+ "members": [attestation],
1020
+ "membersComplete": True,
1021
+ "leaderExited": False,
1022
+ }
1023
+ else:
1024
+ try:
1025
+ process.kill()
1026
+ process.wait(timeout=5)
1027
+ except (OSError, subprocess.TimeoutExpired):
1028
+ pass
1029
+ job.close()
1030
+ raise RuntimeError("IDENTITY_UNVERIFIABLE: Windows ownership proof unavailable")
1031
+ else:
1032
+ tree = observed.get("treeIdentity") or {}
1033
+ kind = "POSIX_MEMBERS"
1034
+ if sys.platform.startswith("linux") and hasattr(os, "pidfd_open"):
1035
+ try:
1036
+ pidfd = os.pidfd_open(process.pid)
1037
+ kind = "LINUX_PIDFD_SESSION"
1038
+ except OSError:
1039
+ pidfd = None
1040
+ proof = {
1041
+ "schemaVersion": 1,
1042
+ "proofId": proof_id,
1043
+ "kind": kind,
1044
+ "ownershipTokenHash": owner_hash,
1045
+ "leaderPid": process.pid,
1046
+ "leaderCreatedAt": attestation.get("createdAt"),
1047
+ "groupId": tree.get("groupId", process.pid),
1048
+ "sessionId": tree.get("sessionId", process.pid),
1049
+ "members": [attestation],
1050
+ "membersComplete": bool(tree.get("complete", False)),
1051
+ "leaderExited": False,
1052
+ "pidfdHeld": pidfd is not None,
1053
+ }
1054
+ attestation["capabilities"]["canVerifyOwnership"] = bool(
1055
+ isinstance(proof, dict)
1056
+ and proof.get("membersComplete") is True
1057
+ )
1058
+ return SpawnedProcess(
1059
+ process=process,
1060
+ attestation=attestation,
1061
+ observedIdentity=observed,
1062
+ ownershipProof=proof,
1063
+ _job=job,
1064
+ _pidfd=pidfd,
1065
+ )