@hunter-harness/workflow-harness 0.2.43 → 0.2.44

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 (274) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/build-profile-v3.schema.json +100 -0
  3. package/harness/bundles/general/claude-code/contracts/fixtures/managed-execution.json +341 -0
  4. package/harness/bundles/general/claude-code/contracts/process-identity.schema.json +132 -0
  5. package/harness/bundles/general/claude-code/contracts/run-session.schema.json +142 -0
  6. package/harness/bundles/general/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
  7. package/harness/bundles/general/claude-code/contracts/service-session.schema.json +147 -0
  8. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-submit/checklist.md +12 -0
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +267 -22
  20. package/harness/bundles/general/claude-code/scripts/harness_change.py +137 -2
  21. package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +153 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_environment.py +55 -25
  23. package/harness/bundles/general/claude-code/scripts/harness_execution_contracts.py +596 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_integration.py +289 -52
  25. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +5 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_orchestration.py +344 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_process.py +1065 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_profile.py +244 -4
  29. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +543 -132
  30. package/harness/bundles/general/claude-code/scripts/harness_service.py +1469 -194
  31. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +107 -117
  32. package/harness/bundles/general/claude-code/scripts/harness_verification.py +79 -0
  33. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  34. package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +100 -0
  35. package/harness/bundles/general/codebuddy/contracts/fixtures/managed-execution.json +341 -0
  36. package/harness/bundles/general/codebuddy/contracts/process-identity.schema.json +132 -0
  37. package/harness/bundles/general/codebuddy/contracts/run-session.schema.json +142 -0
  38. package/harness/bundles/general/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
  39. package/harness/bundles/general/codebuddy/contracts/service-session.schema.json +147 -0
  40. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +12 -0
  49. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  50. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  51. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +267 -22
  52. package/harness/bundles/general/codebuddy/scripts/harness_change.py +137 -2
  53. package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +153 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +55 -25
  55. package/harness/bundles/general/codebuddy/scripts/harness_execution_contracts.py +596 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +289 -52
  57. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +5 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_orchestration.py +344 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_process.py +1065 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +244 -4
  61. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +543 -132
  62. package/harness/bundles/general/codebuddy/scripts/harness_service.py +1469 -194
  63. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +107 -117
  64. package/harness/bundles/general/codebuddy/scripts/harness_verification.py +79 -0
  65. package/harness/bundles/general/codex/.harness-build.json +1 -1
  66. package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +100 -0
  67. package/harness/bundles/general/codex/contracts/fixtures/managed-execution.json +341 -0
  68. package/harness/bundles/general/codex/contracts/process-identity.schema.json +132 -0
  69. package/harness/bundles/general/codex/contracts/run-session.schema.json +142 -0
  70. package/harness/bundles/general/codex/contracts/service-retirement-receipt.schema.json +85 -0
  71. package/harness/bundles/general/codex/contracts/service-session.schema.json +147 -0
  72. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  75. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  77. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  80. package/harness/bundles/general/codex/harness-submit/checklist.md +12 -0
  81. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  82. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  83. package/harness/bundles/general/codex/scripts/harness_archive.py +267 -22
  84. package/harness/bundles/general/codex/scripts/harness_change.py +137 -2
  85. package/harness/bundles/general/codex/scripts/harness_efficiency.py +153 -0
  86. package/harness/bundles/general/codex/scripts/harness_environment.py +55 -25
  87. package/harness/bundles/general/codex/scripts/harness_execution_contracts.py +596 -0
  88. package/harness/bundles/general/codex/scripts/harness_integration.py +289 -52
  89. package/harness/bundles/general/codex/scripts/harness_ledger.py +5 -0
  90. package/harness/bundles/general/codex/scripts/harness_orchestration.py +344 -0
  91. package/harness/bundles/general/codex/scripts/harness_process.py +1065 -0
  92. package/harness/bundles/general/codex/scripts/harness_profile.py +244 -4
  93. package/harness/bundles/general/codex/scripts/harness_runtime.py +543 -132
  94. package/harness/bundles/general/codex/scripts/harness_service.py +1469 -194
  95. package/harness/bundles/general/codex/scripts/harness_test_runner.py +107 -117
  96. package/harness/bundles/general/codex/scripts/harness_verification.py +79 -0
  97. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  98. package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +100 -0
  99. package/harness/bundles/general/cursor/contracts/fixtures/managed-execution.json +341 -0
  100. package/harness/bundles/general/cursor/contracts/process-identity.schema.json +132 -0
  101. package/harness/bundles/general/cursor/contracts/run-session.schema.json +142 -0
  102. package/harness/bundles/general/cursor/contracts/service-retirement-receipt.schema.json +85 -0
  103. package/harness/bundles/general/cursor/contracts/service-session.schema.json +147 -0
  104. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  106. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  107. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  109. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  110. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  111. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  112. package/harness/bundles/general/cursor/harness-submit/checklist.md +12 -0
  113. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  114. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  115. package/harness/bundles/general/cursor/scripts/harness_archive.py +267 -22
  116. package/harness/bundles/general/cursor/scripts/harness_change.py +137 -2
  117. package/harness/bundles/general/cursor/scripts/harness_efficiency.py +153 -0
  118. package/harness/bundles/general/cursor/scripts/harness_environment.py +55 -25
  119. package/harness/bundles/general/cursor/scripts/harness_execution_contracts.py +596 -0
  120. package/harness/bundles/general/cursor/scripts/harness_integration.py +289 -52
  121. package/harness/bundles/general/cursor/scripts/harness_ledger.py +5 -0
  122. package/harness/bundles/general/cursor/scripts/harness_orchestration.py +344 -0
  123. package/harness/bundles/general/cursor/scripts/harness_process.py +1065 -0
  124. package/harness/bundles/general/cursor/scripts/harness_profile.py +244 -4
  125. package/harness/bundles/general/cursor/scripts/harness_runtime.py +543 -132
  126. package/harness/bundles/general/cursor/scripts/harness_service.py +1469 -194
  127. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +107 -117
  128. package/harness/bundles/general/cursor/scripts/harness_verification.py +79 -0
  129. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  130. package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +100 -0
  131. package/harness/bundles/java/claude-code/contracts/fixtures/managed-execution.json +341 -0
  132. package/harness/bundles/java/claude-code/contracts/process-identity.schema.json +132 -0
  133. package/harness/bundles/java/claude-code/contracts/run-session.schema.json +142 -0
  134. package/harness/bundles/java/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
  135. package/harness/bundles/java/claude-code/contracts/service-session.schema.json +147 -0
  136. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  138. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  142. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  144. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  145. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  146. package/harness/bundles/java/claude-code/harness-submit/checklist.md +12 -0
  147. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  148. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  149. package/harness/bundles/java/claude-code/scripts/harness_archive.py +267 -22
  150. package/harness/bundles/java/claude-code/scripts/harness_change.py +137 -2
  151. package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +153 -0
  152. package/harness/bundles/java/claude-code/scripts/harness_environment.py +55 -25
  153. package/harness/bundles/java/claude-code/scripts/harness_execution_contracts.py +596 -0
  154. package/harness/bundles/java/claude-code/scripts/harness_integration.py +289 -52
  155. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +5 -0
  156. package/harness/bundles/java/claude-code/scripts/harness_orchestration.py +344 -0
  157. package/harness/bundles/java/claude-code/scripts/harness_process.py +1065 -0
  158. package/harness/bundles/java/claude-code/scripts/harness_profile.py +244 -4
  159. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +543 -132
  160. package/harness/bundles/java/claude-code/scripts/harness_service.py +1469 -194
  161. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +107 -117
  162. package/harness/bundles/java/claude-code/scripts/harness_verification.py +79 -0
  163. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  164. package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +100 -0
  165. package/harness/bundles/java/codebuddy/contracts/fixtures/managed-execution.json +341 -0
  166. package/harness/bundles/java/codebuddy/contracts/process-identity.schema.json +132 -0
  167. package/harness/bundles/java/codebuddy/contracts/run-session.schema.json +142 -0
  168. package/harness/bundles/java/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
  169. package/harness/bundles/java/codebuddy/contracts/service-session.schema.json +147 -0
  170. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  171. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  172. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  175. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  176. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  178. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  179. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  180. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +12 -0
  181. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  182. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  183. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +267 -22
  184. package/harness/bundles/java/codebuddy/scripts/harness_change.py +137 -2
  185. package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +153 -0
  186. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +55 -25
  187. package/harness/bundles/java/codebuddy/scripts/harness_execution_contracts.py +596 -0
  188. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +289 -52
  189. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +5 -0
  190. package/harness/bundles/java/codebuddy/scripts/harness_orchestration.py +344 -0
  191. package/harness/bundles/java/codebuddy/scripts/harness_process.py +1065 -0
  192. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +244 -4
  193. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +543 -132
  194. package/harness/bundles/java/codebuddy/scripts/harness_service.py +1469 -194
  195. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +107 -117
  196. package/harness/bundles/java/codebuddy/scripts/harness_verification.py +79 -0
  197. package/harness/bundles/java/codex/.harness-build.json +1 -1
  198. package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +100 -0
  199. package/harness/bundles/java/codex/contracts/fixtures/managed-execution.json +341 -0
  200. package/harness/bundles/java/codex/contracts/process-identity.schema.json +132 -0
  201. package/harness/bundles/java/codex/contracts/run-session.schema.json +142 -0
  202. package/harness/bundles/java/codex/contracts/service-retirement-receipt.schema.json +85 -0
  203. package/harness/bundles/java/codex/contracts/service-session.schema.json +147 -0
  204. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  205. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  206. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  208. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  209. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  210. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  211. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  212. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  213. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  214. package/harness/bundles/java/codex/harness-submit/checklist.md +12 -0
  215. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  216. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  217. package/harness/bundles/java/codex/scripts/harness_archive.py +267 -22
  218. package/harness/bundles/java/codex/scripts/harness_change.py +137 -2
  219. package/harness/bundles/java/codex/scripts/harness_efficiency.py +153 -0
  220. package/harness/bundles/java/codex/scripts/harness_environment.py +55 -25
  221. package/harness/bundles/java/codex/scripts/harness_execution_contracts.py +596 -0
  222. package/harness/bundles/java/codex/scripts/harness_integration.py +289 -52
  223. package/harness/bundles/java/codex/scripts/harness_ledger.py +5 -0
  224. package/harness/bundles/java/codex/scripts/harness_orchestration.py +344 -0
  225. package/harness/bundles/java/codex/scripts/harness_process.py +1065 -0
  226. package/harness/bundles/java/codex/scripts/harness_profile.py +244 -4
  227. package/harness/bundles/java/codex/scripts/harness_runtime.py +543 -132
  228. package/harness/bundles/java/codex/scripts/harness_service.py +1469 -194
  229. package/harness/bundles/java/codex/scripts/harness_test_runner.py +107 -117
  230. package/harness/bundles/java/codex/scripts/harness_verification.py +79 -0
  231. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  232. package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +100 -0
  233. package/harness/bundles/java/cursor/contracts/fixtures/managed-execution.json +341 -0
  234. package/harness/bundles/java/cursor/contracts/process-identity.schema.json +132 -0
  235. package/harness/bundles/java/cursor/contracts/run-session.schema.json +142 -0
  236. package/harness/bundles/java/cursor/contracts/service-retirement-receipt.schema.json +85 -0
  237. package/harness/bundles/java/cursor/contracts/service-session.schema.json +147 -0
  238. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  239. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  240. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  241. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  242. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  243. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  244. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  246. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  247. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  248. package/harness/bundles/java/cursor/harness-submit/checklist.md +12 -0
  249. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  250. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  251. package/harness/bundles/java/cursor/scripts/harness_archive.py +267 -22
  252. package/harness/bundles/java/cursor/scripts/harness_change.py +137 -2
  253. package/harness/bundles/java/cursor/scripts/harness_efficiency.py +153 -0
  254. package/harness/bundles/java/cursor/scripts/harness_environment.py +55 -25
  255. package/harness/bundles/java/cursor/scripts/harness_execution_contracts.py +596 -0
  256. package/harness/bundles/java/cursor/scripts/harness_integration.py +289 -52
  257. package/harness/bundles/java/cursor/scripts/harness_ledger.py +5 -0
  258. package/harness/bundles/java/cursor/scripts/harness_orchestration.py +344 -0
  259. package/harness/bundles/java/cursor/scripts/harness_process.py +1065 -0
  260. package/harness/bundles/java/cursor/scripts/harness_profile.py +244 -4
  261. package/harness/bundles/java/cursor/scripts/harness_runtime.py +543 -132
  262. package/harness/bundles/java/cursor/scripts/harness_service.py +1469 -194
  263. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +107 -117
  264. package/harness/bundles/java/cursor/scripts/harness_verification.py +79 -0
  265. package/harness/manifests/general/claude-code.json +58 -26
  266. package/harness/manifests/general/codebuddy.json +58 -26
  267. package/harness/manifests/general/codex.json +58 -26
  268. package/harness/manifests/general/cursor.json +58 -26
  269. package/harness/manifests/java/claude-code.json +60 -28
  270. package/harness/manifests/java/codebuddy.json +60 -28
  271. package/harness/manifests/java/codex.json +60 -28
  272. package/harness/manifests/java/cursor.json +60 -28
  273. package/hunter-workflow-family.json +4 -4
  274. package/package.json +1 -1
@@ -33,6 +33,12 @@ SCHEMA_VERSION = 1
33
33
  RUN_SESSION_SCHEMA_VERSION = 1
34
34
  RUN_TERMINAL_STATUSES = {"OK", "FAIL", "INCOMPLETE", "CANCELLED"}
35
35
  _CHANGE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
36
+ SECRET_SCAN_RULES_VERSION = "secret-scan-v1"
37
+ SECRET_SCAN_RECEIPT_REL = Path("meta") / "secret-scan-receipt.json"
38
+ _SENSITIVE_ASSIGNMENT = re.compile(
39
+ rb"(?i)[\"']?(?:password|passwd|token|secret|cookie|authorization|database[_-]?url)"
40
+ rb"[\"']?\s*[:=]\s*[\"']?[^\s\"']{4,}"
41
+ )
36
42
  _ADAPTERS = {
37
43
  "claude-code": (".worktrees", "harness/"),
38
44
  "codex": (".worktrees", "harness/"),
@@ -116,6 +122,248 @@ def _sha256_json(value: Any) -> str:
116
122
  return "sha256:" + hashlib.sha256(encoded).hexdigest()
117
123
 
118
124
 
125
+ def _publishable_tree_digest(root: Path) -> str:
126
+ """Hash publishable bytes while excluding the self-referential scan receipt."""
127
+ root = root.expanduser().resolve()
128
+ digest = hashlib.sha256()
129
+ receipt = (root / SECRET_SCAN_RECEIPT_REL).resolve()
130
+ if not root.is_dir():
131
+ raise OSError(f"publishable evidence root not found: {root}")
132
+ for path in sorted(root.rglob("*")):
133
+ if not path.is_file() or path.resolve() == receipt:
134
+ continue
135
+ relative = path.relative_to(root).as_posix()
136
+ data = path.read_bytes()
137
+ digest.update(relative.encode("utf-8"))
138
+ digest.update(b"\0")
139
+ digest.update(str(len(data)).encode("ascii"))
140
+ digest.update(b"\0")
141
+ digest.update(hashlib.sha256(data).digest())
142
+ digest.update(b"\0")
143
+ return "sha256:" + digest.hexdigest()
144
+
145
+
146
+ def _sensitive_evidence_receipt_path(change_root: Path) -> Path:
147
+ return change_root.expanduser().resolve() / SECRET_SCAN_RECEIPT_REL
148
+
149
+
150
+ def _private_evidence_root(explicit: Path | None = None) -> Path:
151
+ configured = os.environ.get("HARNESS_PRIVATE_EVIDENCE_ROOT")
152
+ return (explicit if explicit is not None else Path(configured) if configured else
153
+ Path.home() / ".harness" / "private-evidence").expanduser().resolve()
154
+
155
+
156
+ def _secure_private_path(path: Path, *, directory: bool) -> str:
157
+ """Best-effort owner-only permissions, with an explicit ACL result on Windows."""
158
+ mode = 0o700 if directory else 0o600
159
+ try:
160
+ path.chmod(mode)
161
+ except OSError:
162
+ pass
163
+ if os.name != "nt":
164
+ return "POSIX_OWNER_ONLY"
165
+ try:
166
+ account = os.environ.get("USERNAME") or os.environ.get("USER")
167
+ if not account:
168
+ account = subprocess.check_output(
169
+ ["whoami"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8"
170
+ ).strip()
171
+ result = subprocess.run(
172
+ ["icacls", str(path), "/inheritance:r", "/grant:r", f"{account}:F"],
173
+ capture_output=True,
174
+ text=True,
175
+ encoding="utf-8",
176
+ errors="replace",
177
+ check=False,
178
+ timeout=10,
179
+ )
180
+ return "WINDOWS_CURRENT_USER_ONLY" if result.returncode == 0 else "WINDOWS_ACL_UNVERIFIED"
181
+ except (OSError, subprocess.SubprocessError):
182
+ return "WINDOWS_ACL_UNVERIFIED"
183
+
184
+
185
+ def _sensitive_candidates(root: Path) -> list[dict[str, Any]]:
186
+ """Find high-confidence plaintext assignments without treating prose as a secret."""
187
+ receipt = _sensitive_evidence_receipt_path(root)
188
+ candidates: list[dict[str, Any]] = []
189
+ for path in sorted(root.rglob("*")):
190
+ if not path.is_file() or path.resolve() == receipt.resolve():
191
+ continue
192
+ try:
193
+ raw = path.read_bytes()
194
+ except OSError:
195
+ continue
196
+ match = _SENSITIVE_ASSIGNMENT.search(raw)
197
+ if match is None:
198
+ continue
199
+ candidates.append({
200
+ "path": path.relative_to(root).as_posix(),
201
+ "reasonCode": "PLAINTEXT_SENSITIVE_ASSIGNMENT",
202
+ "digest": "sha256:" + hashlib.sha256(raw).hexdigest(),
203
+ })
204
+ return candidates
205
+
206
+
207
+ def publishable_tree_digest(root: Path) -> str:
208
+ """Public digest helper shared by the archive publication gate."""
209
+ return _publishable_tree_digest(root)
210
+
211
+
212
+ def sensitive_evidence_receipt_path(change_root: Path) -> Path:
213
+ return _sensitive_evidence_receipt_path(change_root)
214
+
215
+
216
+ def sensitive_evidence_candidates(root: Path) -> list[dict[str, Any]]:
217
+ return _sensitive_candidates(root)
218
+
219
+
220
+ def _write_sensitive_scan_receipt(
221
+ change_root: Path,
222
+ *,
223
+ entries: list[dict[str, Any]],
224
+ unresolved: list[dict[str, Any]],
225
+ status: str,
226
+ ) -> dict[str, Any]:
227
+ change_root = change_root.expanduser().resolve()
228
+ receipt = {
229
+ "schemaVersion": 1,
230
+ "rulesVersion": SECRET_SCAN_RULES_VERSION,
231
+ "changeId": change_root.name,
232
+ "status": status,
233
+ "unresolvedFailures": unresolved,
234
+ "entries": entries,
235
+ "publishableTreeDigest": _publishable_tree_digest(change_root),
236
+ "publicationExcluded": True,
237
+ "recordedAt": now_iso(),
238
+ }
239
+ atomic_write_json(_sensitive_evidence_receipt_path(change_root), receipt)
240
+ return receipt
241
+
242
+
243
+ def quarantine_sensitive_evidence(
244
+ source: Path,
245
+ *,
246
+ change_root: Path,
247
+ reason: str = "sensitive evidence",
248
+ private_root: Path | None = None,
249
+ ) -> dict[str, Any]:
250
+ """Atomically move legacy plaintext evidence outside any publishable tree."""
251
+ source = source.expanduser().resolve()
252
+ change_root = change_root.expanduser().resolve()
253
+ private = _private_evidence_root(private_root)
254
+ if not source.is_file():
255
+ return {"ok": False, "reasonCode": "SENSITIVE_EVIDENCE_SOURCE_MISSING", "sourcePath": str(source)}
256
+ if private == change_root or private.is_relative_to(change_root):
257
+ return {
258
+ "ok": False,
259
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
260
+ "error": "private quarantine root must be outside the publishable change root",
261
+ }
262
+ try:
263
+ raw_digest = "sha256:" + hashlib.sha256(source.read_bytes()).hexdigest()
264
+ quarantine_dir = private / uuid.uuid4().hex
265
+ quarantine_dir.mkdir(parents=True, exist_ok=False)
266
+ acl_dir = _secure_private_path(quarantine_dir, directory=True)
267
+ target = quarantine_dir / "payload.bin"
268
+ os.replace(source, target)
269
+ acl_file = _secure_private_path(target, directory=False)
270
+ if os.name == "nt" and (
271
+ acl_dir != "WINDOWS_CURRENT_USER_ONLY"
272
+ or acl_file != "WINDOWS_CURRENT_USER_ONLY"
273
+ ):
274
+ os.replace(target, source)
275
+ shutil.rmtree(quarantine_dir, ignore_errors=True)
276
+ return {
277
+ "ok": False,
278
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
279
+ "error": "private quarantine ACL could not be restricted to the current user",
280
+ }
281
+ moved_digest = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
282
+ if moved_digest != raw_digest:
283
+ os.replace(target, source)
284
+ shutil.rmtree(quarantine_dir, ignore_errors=True)
285
+ return {
286
+ "ok": False,
287
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
288
+ "error": "quarantine digest verification failed",
289
+ }
290
+ try:
291
+ prior = json.loads(
292
+ _sensitive_evidence_receipt_path(change_root).read_text(
293
+ encoding="utf-8-sig"
294
+ )
295
+ )
296
+ except (OSError, json.JSONDecodeError):
297
+ prior = {}
298
+ entries = list(prior.get("entries") or []) if isinstance(prior, dict) else []
299
+ entries.append({
300
+ "sourcePath": (
301
+ source.relative_to(change_root).as_posix()
302
+ if source.is_relative_to(change_root)
303
+ else source.name
304
+ ),
305
+ "sourceDigest": raw_digest,
306
+ "privatePath": str(target),
307
+ "reason": reason[:160],
308
+ "status": "QUARANTINED",
309
+ "acl": {"directory": acl_dir, "file": acl_file},
310
+ "movedAt": now_iso(),
311
+ })
312
+ try:
313
+ receipt = _write_sensitive_scan_receipt(
314
+ change_root,
315
+ entries=entries,
316
+ unresolved=[],
317
+ status="QUARANTINED",
318
+ )
319
+ except BaseException as exc:
320
+ try:
321
+ os.replace(target, source)
322
+ except OSError:
323
+ pass
324
+ shutil.rmtree(quarantine_dir, ignore_errors=True)
325
+ return {
326
+ "ok": False,
327
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
328
+ "error": str(exc),
329
+ }
330
+ return {
331
+ "ok": True,
332
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINED",
333
+ "sourceDigest": raw_digest,
334
+ "privatePath": str(target),
335
+ "receiptPath": str(_sensitive_evidence_receipt_path(change_root)),
336
+ "receipt": receipt,
337
+ }
338
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
339
+ # No copy is retained on failure. If os.replace crossed filesystems it
340
+ # fails before removing the source, which is the required fail-closed path.
341
+ return {
342
+ "ok": False,
343
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
344
+ "error": str(exc),
345
+ }
346
+
347
+
348
+ def ensure_sensitive_evidence_scan_receipt(change_root: Path) -> dict[str, Any]:
349
+ """Create a digest-bound scan receipt for a tree with no plaintext findings."""
350
+ change_root = change_root.expanduser().resolve()
351
+ unresolved = _sensitive_candidates(change_root)
352
+ if unresolved:
353
+ return _write_sensitive_scan_receipt(
354
+ change_root,
355
+ entries=[],
356
+ unresolved=unresolved,
357
+ status="FAIL",
358
+ )
359
+ return _write_sensitive_scan_receipt(
360
+ change_root,
361
+ entries=[],
362
+ unresolved=[],
363
+ status="OK",
364
+ )
365
+
366
+
119
367
  def _run_sessions_root(state_root: Path) -> Path:
120
368
  return state_root.expanduser().resolve() / "runtime" / "run-sessions"
121
369
 
@@ -130,17 +378,114 @@ def _run_receipt_path(state_root: Path, session_id: str) -> Path:
130
378
  return _run_session_root(state_root, session_id) / "session.json"
131
379
 
132
380
 
381
+ def _resource_lock_secrets_path(state_root: Path, session_id: str) -> Path:
382
+ return _run_session_root(state_root, session_id) / "resource-lock-secrets.json"
383
+
384
+
133
385
  def _load_run_receipt(state_root: Path, session_id: str) -> dict[str, Any]:
134
386
  path = _run_receipt_path(state_root, session_id)
135
- try:
136
- value = json.loads(path.read_text(encoding="utf-8-sig"))
137
- except FileNotFoundError as exc:
138
- raise ValueError(f"RUN_SESSION_NOT_FOUND: {session_id}") from exc
387
+ last_error: OSError | None = None
388
+ for attempt in range(20):
389
+ try:
390
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
391
+ break
392
+ except FileNotFoundError as exc:
393
+ raise ValueError(f"RUN_SESSION_NOT_FOUND: {session_id}") from exc
394
+ except PermissionError as exc:
395
+ last_error = exc
396
+ if attempt == 19:
397
+ raise ValueError(
398
+ f"RUN_SESSION_RECEIPT_BUSY: {session_id}"
399
+ ) from exc
400
+ time.sleep(0.01 * (attempt + 1))
401
+ else:
402
+ raise ValueError(f"RUN_SESSION_RECEIPT_BUSY: {session_id}") from last_error
139
403
  if not isinstance(value, dict):
140
404
  raise ValueError(f"RUN_SESSION_CORRUPT: {session_id}")
141
405
  return value
142
406
 
143
407
 
408
+ def _load_resource_lock_tokens(
409
+ state_root: Path,
410
+ session_id: str,
411
+ receipt: dict[str, Any],
412
+ ) -> dict[str, str]:
413
+ """Load lock tokens from a private sidecar; legacy receipts may inline them."""
414
+ path = _resource_lock_secrets_path(state_root, session_id)
415
+ try:
416
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
417
+ if isinstance(value, dict) and isinstance(value.get("tokens"), dict):
418
+ return {
419
+ str(name): str(token)
420
+ for name, token in value["tokens"].items()
421
+ if isinstance(name, str) and isinstance(token, str)
422
+ }
423
+ except (OSError, json.JSONDecodeError, TypeError):
424
+ pass
425
+ legacy = receipt.get("resourceLockTokens")
426
+ if isinstance(legacy, dict):
427
+ return {
428
+ str(name): str(token)
429
+ for name, token in legacy.items()
430
+ if isinstance(name, str) and isinstance(token, str)
431
+ }
432
+ return {}
433
+
434
+
435
+ def _receipt_identity_state(
436
+ receipt: dict[str, Any],
437
+ *,
438
+ field: str = "workerIdentity",
439
+ ) -> bool | None:
440
+ """Verify a canonical provider attestation, with legacy facade fallback."""
441
+ identity = receipt.get(field)
442
+ pid = identity.get("pid") if isinstance(identity, dict) else receipt.get("workerPid")
443
+ if isinstance(identity, dict) and {
444
+ "schemaVersion",
445
+ "createdAt",
446
+ "fieldProvenance",
447
+ "capabilities",
448
+ } <= set(identity):
449
+ from harness_process import observe_process_identity, verify_process_identity
450
+
451
+ observed = observe_process_identity(pid)
452
+ decision = verify_process_identity(identity, observed)
453
+ if decision.get("ok") is True:
454
+ return True
455
+ if decision.get("reasonCode") == "IDENTITY_UNVERIFIABLE":
456
+ return None
457
+ return False
458
+ from harness_service import verify_process_identity
459
+
460
+ legacy = {
461
+ "pid": pid,
462
+ "startedAt": (
463
+ identity.get("startedAt")
464
+ if isinstance(identity, dict)
465
+ else None
466
+ ),
467
+ "processIdentity": {
468
+ "executable": (
469
+ identity.get("executable")
470
+ if isinstance(identity, dict)
471
+ else None
472
+ )
473
+ },
474
+ }
475
+ return verify_process_identity(legacy)
476
+
477
+
478
+ def _capture_provider_members(spawned: Any) -> None:
479
+ """Persist current provider-owned members before a later cleanup."""
480
+ proof = getattr(spawned, "ownershipProof", None)
481
+ leader = getattr(spawned, "attestation", None)
482
+ if not isinstance(proof, dict) or not isinstance(leader, dict):
483
+ return
484
+ from harness_process import capture_owned_members
485
+
486
+ capture_owned_members(spawned)
487
+
488
+
144
489
  def _resource_lock_root() -> Path:
145
490
  return Path(
146
491
  os.environ.get(
@@ -366,25 +711,86 @@ def _acquire_resource_locks(
366
711
 
367
712
 
368
713
  def _write_run_receipt(state_root: Path, receipt: dict[str, Any]) -> None:
714
+ _update_receipt_contract(receipt)
715
+ publishable = json.loads(json.dumps(receipt, ensure_ascii=False))
716
+ raw_tokens = publishable.pop("resourceLockTokens", {})
717
+ if isinstance(raw_tokens, dict):
718
+ publishable["resourceLockTokenHashes"] = {
719
+ str(name): _sha256_json(str(token))
720
+ for name, token in raw_tokens.items()
721
+ if isinstance(name, str) and isinstance(token, str)
722
+ }
369
723
  atomic_write_json(
370
724
  _run_receipt_path(state_root, str(receipt["sessionId"])),
371
- receipt,
725
+ publishable,
372
726
  )
373
727
 
374
728
 
375
729
  def _process_identity(pid: int, executable_hint: str) -> dict[str, Any]:
376
- from harness_service import get_process_create_time, get_process_executable
377
-
378
- created = get_process_create_time(pid)
379
- executable = get_process_executable(pid)
730
+ from harness_process import observe_process_identity
731
+
732
+ observed = observe_process_identity(pid)
733
+ if not observed.get("executable"):
734
+ observed["executable"] = _absolute_executable(executable_hint)
735
+ observed["fieldProvenance"]["executable"] = "ATTESTED"
736
+ if not observed.get("createdAt"):
737
+ observed["createdAt"] = now_iso()
738
+ observed["fieldProvenance"]["createdAt"] = "ATTESTED"
739
+ # ``startedAt`` is a compatibility alias used by legacy session readers;
740
+ # canonical identity comparisons use ``createdAt`` and provenance.
741
+ observed["startedAt"] = observed.get("createdAt")
742
+ return observed
743
+
744
+
745
+ def _log_contract(path: Path) -> dict[str, Any]:
746
+ raw = b""
747
+ if path.is_file():
748
+ try:
749
+ raw = path.read_bytes()
750
+ except OSError:
751
+ raw = b""
752
+ try:
753
+ raw.decode("utf-8")
754
+ decode_status = "OK"
755
+ except UnicodeDecodeError:
756
+ decode_status = "LOG_DECODE_DEGRADED"
380
757
  return {
381
- "pid": pid,
382
- "startedAt": (
383
- created.astimezone().isoformat(timespec="milliseconds")
384
- if created is not None
385
- else now_iso()
758
+ "cursor": len(raw),
759
+ "rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
760
+ "decodeStatus": decode_status,
761
+ }
762
+
763
+
764
+ def _update_receipt_contract(receipt: dict[str, Any]) -> None:
765
+ worker = receipt.get("workerIdentity")
766
+ writer_source = worker if isinstance(worker, dict) else {
767
+ "pid": receipt.get("workerPid") or receipt.get("launcherPid")
768
+ }
769
+ heartbeat_kind = "WORKER" if isinstance(worker, dict) else "SUPERVISOR"
770
+ receipt["heartbeat"] = {
771
+ "kind": heartbeat_kind,
772
+ "writerIdentity": _sha256_json(writer_source),
773
+ "lastSeenAt": str(receipt.get("lastHeartbeatAt") or now_iso()),
774
+ "ttlSeconds": max(1, int(float(receipt.get("heartbeatSeconds") or 30))),
775
+ "staleReason": (
776
+ "SERVICE_HEARTBEAT_STALE"
777
+ if receipt.get("reasonCode") == "HEARTBEAT_LOST"
778
+ else None
386
779
  ),
387
- "executable": executable or _absolute_executable(executable_hint),
780
+ }
781
+ receipt["logs"] = {
782
+ "stdout": _log_contract(Path(str(receipt.get("stdoutPath") or ""))),
783
+ "stderr": _log_contract(Path(str(receipt.get("stderrPath") or ""))),
784
+ }
785
+ cleanup_status = str(receipt.get("cleanupStatus") or "")
786
+ complete = cleanup_status in {
787
+ "NO_CHILD",
788
+ "PROCESS_EXITED",
789
+ "PROCESS_TREE_TERMINATED",
790
+ }
791
+ receipt["cleanup"] = {
792
+ "complete": complete,
793
+ "reasonCode": None if complete else receipt.get("reasonCode"),
388
794
  }
389
795
 
390
796
 
@@ -537,6 +943,14 @@ def start_run_session(
537
943
  _write_run_receipt(state_root, receipt)
538
944
  return receipt
539
945
  receipt["resourceLockTokens"] = lock_tokens
946
+ atomic_write_json(
947
+ _resource_lock_secrets_path(state_root, session_id),
948
+ {"schemaVersion": 1, "tokens": lock_tokens},
949
+ )
950
+ try:
951
+ _resource_lock_secrets_path(state_root, session_id).chmod(0o600)
952
+ except OSError:
953
+ pass
540
954
  _write_run_receipt(state_root, receipt)
541
955
  atomic_write_json(
542
956
  spec_path,
@@ -575,6 +989,7 @@ def start_run_session(
575
989
  )
576
990
  except OSError as exc:
577
991
  spec_path.unlink(missing_ok=True)
992
+ _resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
578
993
  locks_released = _release_resource_locks(
579
994
  state_root,
580
995
  session_id,
@@ -613,22 +1028,32 @@ def start_run_session(
613
1028
  def _terminate_managed_child(
614
1029
  process: subprocess.Popen[bytes],
615
1030
  job: Any,
1031
+ *,
1032
+ attestation: dict[str, Any] | None = None,
1033
+ ownership_proof: dict[str, Any] | None = None,
616
1034
  ) -> bool:
617
- from harness_service import is_pid_alive, terminate_process_tree
1035
+ from harness_process import terminate_owned_tree
618
1036
 
619
- if job is not None and getattr(job, "handle", None) is not None:
620
- job.terminate_and_wait()
621
- try:
622
- process.wait(timeout=5)
623
- except subprocess.TimeoutExpired:
1037
+ if isinstance(attestation, dict) and isinstance(ownership_proof, dict):
1038
+ result = terminate_owned_tree(
1039
+ attestation,
1040
+ ownership_proof,
1041
+ {"graceSeconds": 3.0},
1042
+ )
1043
+ if not result.get("cleanupComplete"):
624
1044
  return False
625
- return not is_pid_alive(process.pid)
626
- terminate_process_tree(process.pid)
1045
+ elif job is not None and getattr(job, "handle", None) is not None:
1046
+ # Compatibility for callers still passing the old Windows job object;
1047
+ # this branch is only used for an already-owned handle, never a PID.
1048
+ job.terminate_and_wait()
1049
+ else:
1050
+ # PID-only cleanup is intentionally forbidden by the process provider.
1051
+ return False
627
1052
  try:
628
1053
  process.wait(timeout=5)
629
1054
  except subprocess.TimeoutExpired:
630
1055
  return False
631
- return not is_pid_alive(process.pid)
1056
+ return process.poll() is not None
632
1057
 
633
1058
 
634
1059
  def _run_session_worker(state_root: Path, session_id: str) -> int:
@@ -637,6 +1062,11 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
637
1062
  session_root = _run_session_root(state_root, session_id)
638
1063
  spec_path = session_root / "launch-spec.json"
639
1064
  receipt = _load_run_receipt(state_root, session_id)
1065
+ receipt["resourceLockTokens"] = _load_resource_lock_tokens(
1066
+ state_root,
1067
+ session_id,
1068
+ receipt,
1069
+ )
640
1070
  try:
641
1071
  spec = json.loads(spec_path.read_text(encoding="utf-8-sig"))
642
1072
  except (OSError, json.JSONDecodeError) as exc:
@@ -690,27 +1120,32 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
690
1120
  stderr_path = Path(str(receipt["stderrPath"]))
691
1121
  stdout_path.parent.mkdir(parents=True, exist_ok=True)
692
1122
  job = None
1123
+ spawned = None
1124
+ ownership_proof: dict[str, Any] | None = None
1125
+ identity: dict[str, Any] | None = None
693
1126
  process: subprocess.Popen[bytes] | None = None
694
1127
  try:
695
1128
  with stdout_path.open("wb", buffering=0) as stdout, stderr_path.open(
696
1129
  "wb", buffering=0
697
1130
  ) as stderr:
698
- popen_kwargs: dict[str, Any] = {
699
- "cwd": str(receipt["workingDirectory"]),
700
- "env": os.environ.copy(),
701
- "stdin": subprocess.DEVNULL,
702
- "stdout": stdout,
703
- "stderr": stderr,
704
- "shell": False,
705
- "close_fds": True,
706
- }
707
- if os.name == "nt":
708
- popen_kwargs["creationflags"] = 0x00000200
709
- else:
710
- popen_kwargs["start_new_session"] = True
711
1131
  try:
712
- process = subprocess.Popen(argv, **popen_kwargs)
713
- except OSError as exc:
1132
+ from harness_process import spawn_structured_argv
1133
+
1134
+ spawned = spawn_structured_argv(
1135
+ argv,
1136
+ cwd=Path(str(receipt["workingDirectory"])),
1137
+ environment={
1138
+ "PYTHONUTF8": "1",
1139
+ "PYTHONIOENCODING": "utf-8",
1140
+ },
1141
+ owner_token=f"run:{session_id}",
1142
+ stdout=stdout,
1143
+ stderr=stderr,
1144
+ )
1145
+ process = spawned.process
1146
+ ownership_proof = spawned.ownershipProof
1147
+ _capture_provider_members(spawned)
1148
+ except (OSError, ValueError, RuntimeError) as exc:
714
1149
  receipt.update(
715
1150
  {
716
1151
  "status": "INCOMPLETE",
@@ -723,56 +1158,19 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
723
1158
  }
724
1159
  )
725
1160
  return 1
726
- identity = _process_identity(process.pid, argv[0])
727
- if os.name == "nt":
728
- from harness_test_runner import _WindowsKillOnCloseJob
729
-
730
- candidate = _WindowsKillOnCloseJob()
731
- if candidate.assign(process):
732
- job = candidate
733
- else:
734
- identity_session = {
735
- "pid": process.pid,
736
- "startedAt": identity["startedAt"],
737
- "processIdentity": {
738
- "executable": identity["executable"],
739
- },
740
- }
741
- verified = verify_process_identity(identity_session) is True
742
- terminated = (
743
- _terminate_managed_child(process, None)
744
- if verified
745
- else False
746
- )
747
- receipt.update(
748
- {
749
- "servicePid": process.pid,
750
- "processIdentity": identity,
751
- "startedAt": identity["startedAt"],
752
- "status": "INCOMPLETE",
753
- "reasonCode": "PROCESS_TREE_ISOLATION_UNAVAILABLE",
754
- "stage": "spawn",
755
- "testProcessStarted": True,
756
- "processTreeIsolated": False,
757
- "exitCode": process.poll(),
758
- "cleanupStatus": (
759
- "PROCESS_TREE_TERMINATED"
760
- if terminated
761
- else "PROCESS_TERMINATION_UNCONFIRMED"
762
- ),
763
- }
764
- )
765
- return 1
1161
+ assert spawned is not None and process is not None
1162
+ identity = spawned.attestation
1163
+ receipt["processOwnershipProof"] = ownership_proof
766
1164
  _stage_transition(receipt, "execute")
767
1165
  receipt.update(
768
1166
  {
769
1167
  "servicePid": process.pid,
770
1168
  "processIdentity": identity,
771
- "startedAt": identity["startedAt"],
1169
+ "startedAt": identity.get("createdAt"),
772
1170
  "status": "RUNNING",
773
1171
  "reasonCode": "CHILD_RUNNING",
774
1172
  "testProcessStarted": True,
775
- "processTreeIsolated": job is not None or os.name != "nt",
1173
+ "processTreeIsolated": ownership_proof is not None,
776
1174
  }
777
1175
  )
778
1176
  _write_run_receipt(state_root, receipt)
@@ -791,15 +1189,18 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
791
1189
  termination_reason = "CANCEL_REQUESTED"
792
1190
  if termination_reason is not None:
793
1191
  diagnostic_path = session_root / "diagnostic.json"
794
- identity_session = {
795
- "pid": process.pid,
796
- "startedAt": identity["startedAt"],
797
- "processIdentity": {
798
- "executable": identity["executable"],
799
- },
800
- }
801
- verified = verify_process_identity(identity_session) is True
802
- job_owned = job is not None
1192
+ from harness_process import (
1193
+ observe_process_identity,
1194
+ verify_process_identity as verify_attestation,
1195
+ )
1196
+
1197
+ verified = verify_attestation(
1198
+ identity,
1199
+ observe_process_identity(process.pid),
1200
+ ).get("ok") is True
1201
+ job_owned = ownership_proof is not None
1202
+ if spawned is not None:
1203
+ _capture_provider_members(spawned)
803
1204
  diagnostic = {
804
1205
  "schemaVersion": 1,
805
1206
  "sessionId": session_id,
@@ -825,8 +1226,13 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
825
1226
  }
826
1227
  atomic_write_json(diagnostic_path, diagnostic)
827
1228
  receipt["diagnosticPath"] = str(diagnostic_path)
828
- if job_owned or verified:
829
- terminated = _terminate_managed_child(process, job)
1229
+ if job_owned and verified:
1230
+ terminated = _terminate_managed_child(
1231
+ process,
1232
+ job,
1233
+ attestation=identity,
1234
+ ownership_proof=ownership_proof,
1235
+ )
830
1236
  job = None
831
1237
  if not terminated:
832
1238
  receipt.update(
@@ -864,7 +1270,14 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
864
1270
  "cleanupStatus": "PROCESS_TREE_TERMINATION_REQUIRED",
865
1271
  }
866
1272
  )
867
- _terminate_managed_child(process, job)
1273
+ if spawned is not None:
1274
+ _capture_provider_members(spawned)
1275
+ _terminate_managed_child(
1276
+ process,
1277
+ job,
1278
+ attestation=identity,
1279
+ ownership_proof=ownership_proof,
1280
+ )
868
1281
  job = None
869
1282
  break
870
1283
  _write_run_receipt(state_root, receipt)
@@ -914,8 +1327,15 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
914
1327
  }
915
1328
  )
916
1329
  finally:
917
- if job is not None:
918
- tree_drained = job.terminate_and_wait()
1330
+ if process is not None and process.poll() is None and ownership_proof is not None:
1331
+ if spawned is not None:
1332
+ _capture_provider_members(spawned)
1333
+ tree_drained = _terminate_managed_child(
1334
+ process,
1335
+ job,
1336
+ attestation=identity if isinstance(identity, dict) else None,
1337
+ ownership_proof=ownership_proof,
1338
+ )
919
1339
  if not tree_drained:
920
1340
  receipt.update(
921
1341
  {
@@ -944,6 +1364,10 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
944
1364
  session_id,
945
1365
  dict(receipt.get("resourceLockTokens") or {}),
946
1366
  )
1367
+ try:
1368
+ _resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
1369
+ except OSError:
1370
+ locks_released = False
947
1371
  if not locks_released:
948
1372
  receipt.update(
949
1373
  {
@@ -977,32 +1401,17 @@ def _run_session_worker_with_log(state_root: Path, session_id: str) -> int:
977
1401
 
978
1402
 
979
1403
  def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
980
- from harness_service import is_pid_alive, verify_process_identity
1404
+ from harness_service import is_pid_alive
981
1405
 
982
1406
  receipt = _load_run_receipt(state_root, session_id)
983
1407
  worker_pid = receipt.get("workerPid")
984
1408
  worker_identity = receipt.get("workerIdentity")
985
- worker_session = {
986
- "pid": worker_pid,
987
- "startedAt": (
988
- worker_identity.get("startedAt")
989
- if isinstance(worker_identity, dict)
990
- else None
991
- ),
992
- "processIdentity": {
993
- "executable": (
994
- worker_identity.get("executable")
995
- if isinstance(worker_identity, dict)
996
- else None
997
- )
998
- },
999
- }
1000
1409
  if receipt.get("status") in RUN_TERMINAL_STATUSES:
1001
1410
  if (
1002
1411
  isinstance(worker_pid, int)
1003
1412
  and worker_pid > 0
1004
1413
  and is_pid_alive(worker_pid)
1005
- and verify_process_identity(worker_session) is True
1414
+ and _receipt_identity_state(receipt) is True
1006
1415
  ):
1007
1416
  finalizing = dict(receipt)
1008
1417
  finalizing.update(
@@ -1036,7 +1445,7 @@ def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
1036
1445
  and heartbeat_age < startup_grace
1037
1446
  ):
1038
1447
  return receipt
1039
- identity_state = verify_process_identity(worker_session)
1448
+ identity_state = _receipt_identity_state(receipt)
1040
1449
  worker_gone = (
1041
1450
  isinstance(worker_pid, int)
1042
1451
  and worker_pid > 0
@@ -1079,6 +1488,7 @@ def read_run_session_log(
1079
1488
  receipt = _load_run_receipt(state_root, session_id)
1080
1489
  path = Path(str(receipt[f"{stream}Path"]))
1081
1490
  raw = b""
1491
+ decode_status = "OK"
1082
1492
  size = path.stat().st_size if path.is_file() else 0
1083
1493
  if path.is_file() and cursor < size:
1084
1494
  with path.open("rb") as handle:
@@ -1090,8 +1500,25 @@ def read_run_session_log(
1090
1500
  break
1091
1501
  except UnicodeDecodeError as exc:
1092
1502
  if exc.end != len(raw):
1503
+ # An invalid sequence in the middle is durable evidence,
1504
+ # so consume it and expose replacement text instead of
1505
+ # silently dropping bytes from the cursor stream.
1093
1506
  text = raw.decode("utf-8", errors="replace")
1507
+ decode_status = "LOG_DECODE_DEGRADED"
1094
1508
  break
1509
+ if (
1510
+ receipt.get("status") in RUN_TERMINAL_STATUSES
1511
+ and cursor + len(raw) >= size
1512
+ ):
1513
+ # Once the producer is terminal, an incomplete trailing
1514
+ # sequence cannot be completed by a later write. Preserve
1515
+ # and consume it with an explicit degraded status.
1516
+ text = raw.decode("utf-8", errors="replace")
1517
+ decode_status = "LOG_DECODE_DEGRADED"
1518
+ break
1519
+ # A live writer may have split a multibyte character across
1520
+ # this read. Keep the incomplete suffix for the next cursor
1521
+ # request so callers never see duplicate replacement glyphs.
1095
1522
  raw = raw[: exc.start]
1096
1523
  else:
1097
1524
  text = ""
@@ -1108,6 +1535,8 @@ def read_run_session_log(
1108
1535
  "text": text,
1109
1536
  "eof": terminal and next_cursor >= size,
1110
1537
  "status": receipt.get("status"),
1538
+ "rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
1539
+ "decodeStatus": decode_status,
1111
1540
  }
1112
1541
 
1113
1542
 
@@ -1117,28 +1546,10 @@ def cancel_run_session(
1117
1546
  *,
1118
1547
  reason: str = "CANCEL_REQUESTED",
1119
1548
  ) -> dict[str, Any]:
1120
- from harness_service import verify_process_identity
1121
-
1122
1549
  receipt = _load_run_receipt(state_root, session_id)
1123
1550
  if receipt.get("status") in RUN_TERMINAL_STATUSES:
1124
1551
  return receipt
1125
- worker_identity = receipt.get("workerIdentity")
1126
- worker_session = {
1127
- "pid": receipt.get("workerPid"),
1128
- "startedAt": (
1129
- worker_identity.get("startedAt")
1130
- if isinstance(worker_identity, dict)
1131
- else None
1132
- ),
1133
- "processIdentity": {
1134
- "executable": (
1135
- worker_identity.get("executable")
1136
- if isinstance(worker_identity, dict)
1137
- else None
1138
- )
1139
- },
1140
- }
1141
- if verify_process_identity(worker_session) is not True:
1552
+ if _receipt_identity_state(receipt) is not True:
1142
1553
  return {
1143
1554
  **receipt,
1144
1555
  "ok": False,