@hunter-harness/workflow-harness 0.2.43 → 0.2.45

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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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 +324 -28
  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 +646 -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,351 @@ 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 _build_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
+ return {
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
+
240
+
241
+ def _write_sensitive_scan_receipt(
242
+ change_root: Path,
243
+ *,
244
+ entries: list[dict[str, Any]],
245
+ unresolved: list[dict[str, Any]],
246
+ status: str,
247
+ ) -> dict[str, Any]:
248
+ change_root = change_root.expanduser().resolve()
249
+ receipt = _build_sensitive_scan_receipt(
250
+ change_root,
251
+ entries=entries,
252
+ unresolved=unresolved,
253
+ status=status,
254
+ )
255
+ atomic_write_json(_sensitive_evidence_receipt_path(change_root), receipt)
256
+ return receipt
257
+
258
+
259
+ def quarantine_sensitive_evidence(
260
+ source: Path,
261
+ *,
262
+ change_root: Path,
263
+ reason: str = "sensitive evidence",
264
+ private_root: Path | None = None,
265
+ ) -> dict[str, Any]:
266
+ """Atomically move legacy plaintext evidence outside any publishable tree."""
267
+ source = source.expanduser().resolve()
268
+ change_root = change_root.expanduser().resolve()
269
+ private = _private_evidence_root(private_root)
270
+ if not source.is_file():
271
+ return {"ok": False, "reasonCode": "SENSITIVE_EVIDENCE_SOURCE_MISSING", "sourcePath": str(source)}
272
+ if private == change_root or private.is_relative_to(change_root):
273
+ return {
274
+ "ok": False,
275
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
276
+ "error": "private quarantine root must be outside the publishable change root",
277
+ }
278
+ try:
279
+ raw_digest = "sha256:" + hashlib.sha256(source.read_bytes()).hexdigest()
280
+ quarantine_dir = private / uuid.uuid4().hex
281
+ quarantine_dir.mkdir(parents=True, exist_ok=False)
282
+ acl_dir = _secure_private_path(quarantine_dir, directory=True)
283
+ target = quarantine_dir / "payload.bin"
284
+ os.replace(source, target)
285
+ acl_file = _secure_private_path(target, directory=False)
286
+ if os.name == "nt" and (
287
+ acl_dir != "WINDOWS_CURRENT_USER_ONLY"
288
+ or acl_file != "WINDOWS_CURRENT_USER_ONLY"
289
+ ):
290
+ os.replace(target, source)
291
+ shutil.rmtree(quarantine_dir, ignore_errors=True)
292
+ return {
293
+ "ok": False,
294
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
295
+ "error": "private quarantine ACL could not be restricted to the current user",
296
+ }
297
+ moved_digest = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
298
+ if moved_digest != raw_digest:
299
+ os.replace(target, source)
300
+ shutil.rmtree(quarantine_dir, ignore_errors=True)
301
+ return {
302
+ "ok": False,
303
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
304
+ "error": "quarantine digest verification failed",
305
+ }
306
+ try:
307
+ prior = json.loads(
308
+ _sensitive_evidence_receipt_path(change_root).read_text(
309
+ encoding="utf-8-sig"
310
+ )
311
+ )
312
+ except (OSError, json.JSONDecodeError):
313
+ prior = {}
314
+ entries = list(prior.get("entries") or []) if isinstance(prior, dict) else []
315
+ entries.append({
316
+ "sourcePath": (
317
+ source.relative_to(change_root).as_posix()
318
+ if source.is_relative_to(change_root)
319
+ else source.name
320
+ ),
321
+ "sourceDigest": raw_digest,
322
+ "privatePath": str(target),
323
+ "reason": reason[:160],
324
+ "status": "QUARANTINED",
325
+ "acl": {"directory": acl_dir, "file": acl_file},
326
+ "movedAt": now_iso(),
327
+ })
328
+ try:
329
+ receipt = _write_sensitive_scan_receipt(
330
+ change_root,
331
+ entries=entries,
332
+ unresolved=[],
333
+ status="QUARANTINED",
334
+ )
335
+ except BaseException as exc:
336
+ try:
337
+ os.replace(target, source)
338
+ except OSError:
339
+ pass
340
+ shutil.rmtree(quarantine_dir, ignore_errors=True)
341
+ return {
342
+ "ok": False,
343
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
344
+ "error": str(exc),
345
+ }
346
+ return {
347
+ "ok": True,
348
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINED",
349
+ "sourceDigest": raw_digest,
350
+ "privatePath": str(target),
351
+ "receiptPath": str(_sensitive_evidence_receipt_path(change_root)),
352
+ "receipt": receipt,
353
+ }
354
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
355
+ # No copy is retained on failure. If os.replace crossed filesystems it
356
+ # fails before removing the source, which is the required fail-closed path.
357
+ return {
358
+ "ok": False,
359
+ "reasonCode": "SENSITIVE_EVIDENCE_QUARANTINE_FAILED",
360
+ "error": str(exc),
361
+ }
362
+
363
+
364
+ def ensure_sensitive_evidence_scan_receipt(change_root: Path) -> dict[str, Any]:
365
+ """Create a digest-bound scan receipt for a tree with no plaintext findings."""
366
+ change_root = change_root.expanduser().resolve()
367
+ unresolved = _sensitive_candidates(change_root)
368
+ if unresolved:
369
+ return _write_sensitive_scan_receipt(
370
+ change_root,
371
+ entries=[],
372
+ unresolved=unresolved,
373
+ status="FAIL",
374
+ )
375
+ return _write_sensitive_scan_receipt(
376
+ change_root,
377
+ entries=[],
378
+ unresolved=[],
379
+ status="OK",
380
+ )
381
+
382
+
383
+ def refresh_sensitive_evidence_scan_receipt(
384
+ change_root: Path,
385
+ *,
386
+ persist: bool = True,
387
+ ) -> dict[str, Any]:
388
+ """Rescan current bytes while preserving valid quarantine audit entries."""
389
+ change_root = change_root.expanduser().resolve()
390
+ receipt_path = _sensitive_evidence_receipt_path(change_root)
391
+ entries: list[dict[str, Any]] = []
392
+ if receipt_path.is_file():
393
+ try:
394
+ prior = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
395
+ except (OSError, json.JSONDecodeError) as exc:
396
+ return {
397
+ "ok": False,
398
+ "reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
399
+ "receiptPath": str(receipt_path),
400
+ "error": str(exc),
401
+ }
402
+ if not isinstance(prior, dict):
403
+ return {
404
+ "ok": False,
405
+ "reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
406
+ "receiptPath": str(receipt_path),
407
+ "error": "receipt must be an object",
408
+ }
409
+ prior_entries = prior.get("entries")
410
+ prior_unresolved = prior.get("unresolvedFailures")
411
+ valid_identity = (
412
+ prior.get("schemaVersion") == 1
413
+ and prior.get("rulesVersion") == SECRET_SCAN_RULES_VERSION
414
+ and prior.get("changeId") == change_root.name
415
+ and prior.get("publicationExcluded") is True
416
+ and str(prior.get("status") or "").upper()
417
+ in {"OK", "QUARANTINED", "FAIL"}
418
+ )
419
+ if (
420
+ not valid_identity
421
+ or not isinstance(prior_entries, list)
422
+ or not isinstance(prior_unresolved, list)
423
+ ):
424
+ return {
425
+ "ok": False,
426
+ "reasonCode": "SECRET_SCAN_RECEIPT_INVALID",
427
+ "receiptPath": str(receipt_path),
428
+ "error": "receipt identity or collection fields are invalid",
429
+ }
430
+ entries = list(prior_entries)
431
+
432
+ unresolved = _sensitive_candidates(change_root)
433
+ status = "FAIL" if unresolved else "QUARANTINED" if entries else "OK"
434
+ if persist:
435
+ try:
436
+ receipt = _write_sensitive_scan_receipt(
437
+ change_root,
438
+ entries=entries,
439
+ unresolved=unresolved,
440
+ status=status,
441
+ )
442
+ except OSError as exc:
443
+ return {
444
+ "ok": False,
445
+ "reasonCode": "SECRET_SCAN_RECEIPT_REFRESH_FAILED",
446
+ "receiptPath": str(receipt_path),
447
+ "error": str(exc),
448
+ }
449
+ else:
450
+ receipt = _build_sensitive_scan_receipt(
451
+ change_root,
452
+ entries=entries,
453
+ unresolved=unresolved,
454
+ status=status,
455
+ )
456
+ return {
457
+ "ok": not unresolved,
458
+ "reasonCode": (
459
+ "SENSITIVE_EVIDENCE_UNQUARANTINED"
460
+ if unresolved
461
+ else "SECRET_SCAN_RECEIPT_REFRESHED"
462
+ ),
463
+ "receiptPath": str(receipt_path),
464
+ "receipt": receipt,
465
+ "persisted": persist,
466
+ "unresolvedFailures": unresolved,
467
+ }
468
+
469
+
119
470
  def _run_sessions_root(state_root: Path) -> Path:
120
471
  return state_root.expanduser().resolve() / "runtime" / "run-sessions"
121
472
 
@@ -130,17 +481,114 @@ def _run_receipt_path(state_root: Path, session_id: str) -> Path:
130
481
  return _run_session_root(state_root, session_id) / "session.json"
131
482
 
132
483
 
484
+ def _resource_lock_secrets_path(state_root: Path, session_id: str) -> Path:
485
+ return _run_session_root(state_root, session_id) / "resource-lock-secrets.json"
486
+
487
+
133
488
  def _load_run_receipt(state_root: Path, session_id: str) -> dict[str, Any]:
134
489
  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
490
+ last_error: OSError | None = None
491
+ for attempt in range(20):
492
+ try:
493
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
494
+ break
495
+ except FileNotFoundError as exc:
496
+ raise ValueError(f"RUN_SESSION_NOT_FOUND: {session_id}") from exc
497
+ except PermissionError as exc:
498
+ last_error = exc
499
+ if attempt == 19:
500
+ raise ValueError(
501
+ f"RUN_SESSION_RECEIPT_BUSY: {session_id}"
502
+ ) from exc
503
+ time.sleep(0.01 * (attempt + 1))
504
+ else:
505
+ raise ValueError(f"RUN_SESSION_RECEIPT_BUSY: {session_id}") from last_error
139
506
  if not isinstance(value, dict):
140
507
  raise ValueError(f"RUN_SESSION_CORRUPT: {session_id}")
141
508
  return value
142
509
 
143
510
 
511
+ def _load_resource_lock_tokens(
512
+ state_root: Path,
513
+ session_id: str,
514
+ receipt: dict[str, Any],
515
+ ) -> dict[str, str]:
516
+ """Load lock tokens from a private sidecar; legacy receipts may inline them."""
517
+ path = _resource_lock_secrets_path(state_root, session_id)
518
+ try:
519
+ value = json.loads(path.read_text(encoding="utf-8-sig"))
520
+ if isinstance(value, dict) and isinstance(value.get("tokens"), dict):
521
+ return {
522
+ str(name): str(token)
523
+ for name, token in value["tokens"].items()
524
+ if isinstance(name, str) and isinstance(token, str)
525
+ }
526
+ except (OSError, json.JSONDecodeError, TypeError):
527
+ pass
528
+ legacy = receipt.get("resourceLockTokens")
529
+ if isinstance(legacy, dict):
530
+ return {
531
+ str(name): str(token)
532
+ for name, token in legacy.items()
533
+ if isinstance(name, str) and isinstance(token, str)
534
+ }
535
+ return {}
536
+
537
+
538
+ def _receipt_identity_state(
539
+ receipt: dict[str, Any],
540
+ *,
541
+ field: str = "workerIdentity",
542
+ ) -> bool | None:
543
+ """Verify a canonical provider attestation, with legacy facade fallback."""
544
+ identity = receipt.get(field)
545
+ pid = identity.get("pid") if isinstance(identity, dict) else receipt.get("workerPid")
546
+ if isinstance(identity, dict) and {
547
+ "schemaVersion",
548
+ "createdAt",
549
+ "fieldProvenance",
550
+ "capabilities",
551
+ } <= set(identity):
552
+ from harness_process import observe_process_identity, verify_process_identity
553
+
554
+ observed = observe_process_identity(pid)
555
+ decision = verify_process_identity(identity, observed)
556
+ if decision.get("ok") is True:
557
+ return True
558
+ if decision.get("reasonCode") == "IDENTITY_UNVERIFIABLE":
559
+ return None
560
+ return False
561
+ from harness_service import verify_process_identity
562
+
563
+ legacy = {
564
+ "pid": pid,
565
+ "startedAt": (
566
+ identity.get("startedAt")
567
+ if isinstance(identity, dict)
568
+ else None
569
+ ),
570
+ "processIdentity": {
571
+ "executable": (
572
+ identity.get("executable")
573
+ if isinstance(identity, dict)
574
+ else None
575
+ )
576
+ },
577
+ }
578
+ return verify_process_identity(legacy)
579
+
580
+
581
+ def _capture_provider_members(spawned: Any) -> None:
582
+ """Persist current provider-owned members before a later cleanup."""
583
+ proof = getattr(spawned, "ownershipProof", None)
584
+ leader = getattr(spawned, "attestation", None)
585
+ if not isinstance(proof, dict) or not isinstance(leader, dict):
586
+ return
587
+ from harness_process import capture_owned_members
588
+
589
+ capture_owned_members(spawned)
590
+
591
+
144
592
  def _resource_lock_root() -> Path:
145
593
  return Path(
146
594
  os.environ.get(
@@ -366,25 +814,86 @@ def _acquire_resource_locks(
366
814
 
367
815
 
368
816
  def _write_run_receipt(state_root: Path, receipt: dict[str, Any]) -> None:
817
+ _update_receipt_contract(receipt)
818
+ publishable = json.loads(json.dumps(receipt, ensure_ascii=False))
819
+ raw_tokens = publishable.pop("resourceLockTokens", {})
820
+ if isinstance(raw_tokens, dict):
821
+ publishable["resourceLockTokenHashes"] = {
822
+ str(name): _sha256_json(str(token))
823
+ for name, token in raw_tokens.items()
824
+ if isinstance(name, str) and isinstance(token, str)
825
+ }
369
826
  atomic_write_json(
370
827
  _run_receipt_path(state_root, str(receipt["sessionId"])),
371
- receipt,
828
+ publishable,
372
829
  )
373
830
 
374
831
 
375
832
  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)
833
+ from harness_process import observe_process_identity
834
+
835
+ observed = observe_process_identity(pid)
836
+ if not observed.get("executable"):
837
+ observed["executable"] = _absolute_executable(executable_hint)
838
+ observed["fieldProvenance"]["executable"] = "ATTESTED"
839
+ if not observed.get("createdAt"):
840
+ observed["createdAt"] = now_iso()
841
+ observed["fieldProvenance"]["createdAt"] = "ATTESTED"
842
+ # ``startedAt`` is a compatibility alias used by legacy session readers;
843
+ # canonical identity comparisons use ``createdAt`` and provenance.
844
+ observed["startedAt"] = observed.get("createdAt")
845
+ return observed
846
+
847
+
848
+ def _log_contract(path: Path) -> dict[str, Any]:
849
+ raw = b""
850
+ if path.is_file():
851
+ try:
852
+ raw = path.read_bytes()
853
+ except OSError:
854
+ raw = b""
855
+ try:
856
+ raw.decode("utf-8")
857
+ decode_status = "OK"
858
+ except UnicodeDecodeError:
859
+ decode_status = "LOG_DECODE_DEGRADED"
380
860
  return {
381
- "pid": pid,
382
- "startedAt": (
383
- created.astimezone().isoformat(timespec="milliseconds")
384
- if created is not None
385
- else now_iso()
861
+ "cursor": len(raw),
862
+ "rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
863
+ "decodeStatus": decode_status,
864
+ }
865
+
866
+
867
+ def _update_receipt_contract(receipt: dict[str, Any]) -> None:
868
+ worker = receipt.get("workerIdentity")
869
+ writer_source = worker if isinstance(worker, dict) else {
870
+ "pid": receipt.get("workerPid") or receipt.get("launcherPid")
871
+ }
872
+ heartbeat_kind = "WORKER" if isinstance(worker, dict) else "SUPERVISOR"
873
+ receipt["heartbeat"] = {
874
+ "kind": heartbeat_kind,
875
+ "writerIdentity": _sha256_json(writer_source),
876
+ "lastSeenAt": str(receipt.get("lastHeartbeatAt") or now_iso()),
877
+ "ttlSeconds": max(1, int(float(receipt.get("heartbeatSeconds") or 30))),
878
+ "staleReason": (
879
+ "SERVICE_HEARTBEAT_STALE"
880
+ if receipt.get("reasonCode") == "HEARTBEAT_LOST"
881
+ else None
386
882
  ),
387
- "executable": executable or _absolute_executable(executable_hint),
883
+ }
884
+ receipt["logs"] = {
885
+ "stdout": _log_contract(Path(str(receipt.get("stdoutPath") or ""))),
886
+ "stderr": _log_contract(Path(str(receipt.get("stderrPath") or ""))),
887
+ }
888
+ cleanup_status = str(receipt.get("cleanupStatus") or "")
889
+ complete = cleanup_status in {
890
+ "NO_CHILD",
891
+ "PROCESS_EXITED",
892
+ "PROCESS_TREE_TERMINATED",
893
+ }
894
+ receipt["cleanup"] = {
895
+ "complete": complete,
896
+ "reasonCode": None if complete else receipt.get("reasonCode"),
388
897
  }
389
898
 
390
899
 
@@ -537,6 +1046,14 @@ def start_run_session(
537
1046
  _write_run_receipt(state_root, receipt)
538
1047
  return receipt
539
1048
  receipt["resourceLockTokens"] = lock_tokens
1049
+ atomic_write_json(
1050
+ _resource_lock_secrets_path(state_root, session_id),
1051
+ {"schemaVersion": 1, "tokens": lock_tokens},
1052
+ )
1053
+ try:
1054
+ _resource_lock_secrets_path(state_root, session_id).chmod(0o600)
1055
+ except OSError:
1056
+ pass
540
1057
  _write_run_receipt(state_root, receipt)
541
1058
  atomic_write_json(
542
1059
  spec_path,
@@ -575,6 +1092,7 @@ def start_run_session(
575
1092
  )
576
1093
  except OSError as exc:
577
1094
  spec_path.unlink(missing_ok=True)
1095
+ _resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
578
1096
  locks_released = _release_resource_locks(
579
1097
  state_root,
580
1098
  session_id,
@@ -613,22 +1131,32 @@ def start_run_session(
613
1131
  def _terminate_managed_child(
614
1132
  process: subprocess.Popen[bytes],
615
1133
  job: Any,
1134
+ *,
1135
+ attestation: dict[str, Any] | None = None,
1136
+ ownership_proof: dict[str, Any] | None = None,
616
1137
  ) -> bool:
617
- from harness_service import is_pid_alive, terminate_process_tree
1138
+ from harness_process import terminate_owned_tree
618
1139
 
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:
1140
+ if isinstance(attestation, dict) and isinstance(ownership_proof, dict):
1141
+ result = terminate_owned_tree(
1142
+ attestation,
1143
+ ownership_proof,
1144
+ {"graceSeconds": 3.0},
1145
+ )
1146
+ if not result.get("cleanupComplete"):
624
1147
  return False
625
- return not is_pid_alive(process.pid)
626
- terminate_process_tree(process.pid)
1148
+ elif job is not None and getattr(job, "handle", None) is not None:
1149
+ # Compatibility for callers still passing the old Windows job object;
1150
+ # this branch is only used for an already-owned handle, never a PID.
1151
+ job.terminate_and_wait()
1152
+ else:
1153
+ # PID-only cleanup is intentionally forbidden by the process provider.
1154
+ return False
627
1155
  try:
628
1156
  process.wait(timeout=5)
629
1157
  except subprocess.TimeoutExpired:
630
1158
  return False
631
- return not is_pid_alive(process.pid)
1159
+ return process.poll() is not None
632
1160
 
633
1161
 
634
1162
  def _run_session_worker(state_root: Path, session_id: str) -> int:
@@ -637,6 +1165,11 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
637
1165
  session_root = _run_session_root(state_root, session_id)
638
1166
  spec_path = session_root / "launch-spec.json"
639
1167
  receipt = _load_run_receipt(state_root, session_id)
1168
+ receipt["resourceLockTokens"] = _load_resource_lock_tokens(
1169
+ state_root,
1170
+ session_id,
1171
+ receipt,
1172
+ )
640
1173
  try:
641
1174
  spec = json.loads(spec_path.read_text(encoding="utf-8-sig"))
642
1175
  except (OSError, json.JSONDecodeError) as exc:
@@ -690,27 +1223,32 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
690
1223
  stderr_path = Path(str(receipt["stderrPath"]))
691
1224
  stdout_path.parent.mkdir(parents=True, exist_ok=True)
692
1225
  job = None
1226
+ spawned = None
1227
+ ownership_proof: dict[str, Any] | None = None
1228
+ identity: dict[str, Any] | None = None
693
1229
  process: subprocess.Popen[bytes] | None = None
694
1230
  try:
695
1231
  with stdout_path.open("wb", buffering=0) as stdout, stderr_path.open(
696
1232
  "wb", buffering=0
697
1233
  ) 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
1234
  try:
712
- process = subprocess.Popen(argv, **popen_kwargs)
713
- except OSError as exc:
1235
+ from harness_process import spawn_structured_argv
1236
+
1237
+ spawned = spawn_structured_argv(
1238
+ argv,
1239
+ cwd=Path(str(receipt["workingDirectory"])),
1240
+ environment={
1241
+ "PYTHONUTF8": "1",
1242
+ "PYTHONIOENCODING": "utf-8",
1243
+ },
1244
+ owner_token=f"run:{session_id}",
1245
+ stdout=stdout,
1246
+ stderr=stderr,
1247
+ )
1248
+ process = spawned.process
1249
+ ownership_proof = spawned.ownershipProof
1250
+ _capture_provider_members(spawned)
1251
+ except (OSError, ValueError, RuntimeError) as exc:
714
1252
  receipt.update(
715
1253
  {
716
1254
  "status": "INCOMPLETE",
@@ -723,56 +1261,19 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
723
1261
  }
724
1262
  )
725
1263
  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
1264
+ assert spawned is not None and process is not None
1265
+ identity = spawned.attestation
1266
+ receipt["processOwnershipProof"] = ownership_proof
766
1267
  _stage_transition(receipt, "execute")
767
1268
  receipt.update(
768
1269
  {
769
1270
  "servicePid": process.pid,
770
1271
  "processIdentity": identity,
771
- "startedAt": identity["startedAt"],
1272
+ "startedAt": identity.get("createdAt"),
772
1273
  "status": "RUNNING",
773
1274
  "reasonCode": "CHILD_RUNNING",
774
1275
  "testProcessStarted": True,
775
- "processTreeIsolated": job is not None or os.name != "nt",
1276
+ "processTreeIsolated": ownership_proof is not None,
776
1277
  }
777
1278
  )
778
1279
  _write_run_receipt(state_root, receipt)
@@ -791,15 +1292,18 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
791
1292
  termination_reason = "CANCEL_REQUESTED"
792
1293
  if termination_reason is not None:
793
1294
  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
1295
+ from harness_process import (
1296
+ observe_process_identity,
1297
+ verify_process_identity as verify_attestation,
1298
+ )
1299
+
1300
+ verified = verify_attestation(
1301
+ identity,
1302
+ observe_process_identity(process.pid),
1303
+ ).get("ok") is True
1304
+ job_owned = ownership_proof is not None
1305
+ if spawned is not None:
1306
+ _capture_provider_members(spawned)
803
1307
  diagnostic = {
804
1308
  "schemaVersion": 1,
805
1309
  "sessionId": session_id,
@@ -825,8 +1329,13 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
825
1329
  }
826
1330
  atomic_write_json(diagnostic_path, diagnostic)
827
1331
  receipt["diagnosticPath"] = str(diagnostic_path)
828
- if job_owned or verified:
829
- terminated = _terminate_managed_child(process, job)
1332
+ if job_owned and verified:
1333
+ terminated = _terminate_managed_child(
1334
+ process,
1335
+ job,
1336
+ attestation=identity,
1337
+ ownership_proof=ownership_proof,
1338
+ )
830
1339
  job = None
831
1340
  if not terminated:
832
1341
  receipt.update(
@@ -864,7 +1373,14 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
864
1373
  "cleanupStatus": "PROCESS_TREE_TERMINATION_REQUIRED",
865
1374
  }
866
1375
  )
867
- _terminate_managed_child(process, job)
1376
+ if spawned is not None:
1377
+ _capture_provider_members(spawned)
1378
+ _terminate_managed_child(
1379
+ process,
1380
+ job,
1381
+ attestation=identity,
1382
+ ownership_proof=ownership_proof,
1383
+ )
868
1384
  job = None
869
1385
  break
870
1386
  _write_run_receipt(state_root, receipt)
@@ -914,8 +1430,15 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
914
1430
  }
915
1431
  )
916
1432
  finally:
917
- if job is not None:
918
- tree_drained = job.terminate_and_wait()
1433
+ if process is not None and process.poll() is None and ownership_proof is not None:
1434
+ if spawned is not None:
1435
+ _capture_provider_members(spawned)
1436
+ tree_drained = _terminate_managed_child(
1437
+ process,
1438
+ job,
1439
+ attestation=identity if isinstance(identity, dict) else None,
1440
+ ownership_proof=ownership_proof,
1441
+ )
919
1442
  if not tree_drained:
920
1443
  receipt.update(
921
1444
  {
@@ -944,6 +1467,10 @@ def _run_session_worker(state_root: Path, session_id: str) -> int:
944
1467
  session_id,
945
1468
  dict(receipt.get("resourceLockTokens") or {}),
946
1469
  )
1470
+ try:
1471
+ _resource_lock_secrets_path(state_root, session_id).unlink(missing_ok=True)
1472
+ except OSError:
1473
+ locks_released = False
947
1474
  if not locks_released:
948
1475
  receipt.update(
949
1476
  {
@@ -977,32 +1504,17 @@ def _run_session_worker_with_log(state_root: Path, session_id: str) -> int:
977
1504
 
978
1505
 
979
1506
  def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
980
- from harness_service import is_pid_alive, verify_process_identity
1507
+ from harness_service import is_pid_alive
981
1508
 
982
1509
  receipt = _load_run_receipt(state_root, session_id)
983
1510
  worker_pid = receipt.get("workerPid")
984
1511
  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
1512
  if receipt.get("status") in RUN_TERMINAL_STATUSES:
1001
1513
  if (
1002
1514
  isinstance(worker_pid, int)
1003
1515
  and worker_pid > 0
1004
1516
  and is_pid_alive(worker_pid)
1005
- and verify_process_identity(worker_session) is True
1517
+ and _receipt_identity_state(receipt) is True
1006
1518
  ):
1007
1519
  finalizing = dict(receipt)
1008
1520
  finalizing.update(
@@ -1036,7 +1548,7 @@ def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
1036
1548
  and heartbeat_age < startup_grace
1037
1549
  ):
1038
1550
  return receipt
1039
- identity_state = verify_process_identity(worker_session)
1551
+ identity_state = _receipt_identity_state(receipt)
1040
1552
  worker_gone = (
1041
1553
  isinstance(worker_pid, int)
1042
1554
  and worker_pid > 0
@@ -1079,6 +1591,7 @@ def read_run_session_log(
1079
1591
  receipt = _load_run_receipt(state_root, session_id)
1080
1592
  path = Path(str(receipt[f"{stream}Path"]))
1081
1593
  raw = b""
1594
+ decode_status = "OK"
1082
1595
  size = path.stat().st_size if path.is_file() else 0
1083
1596
  if path.is_file() and cursor < size:
1084
1597
  with path.open("rb") as handle:
@@ -1090,8 +1603,25 @@ def read_run_session_log(
1090
1603
  break
1091
1604
  except UnicodeDecodeError as exc:
1092
1605
  if exc.end != len(raw):
1606
+ # An invalid sequence in the middle is durable evidence,
1607
+ # so consume it and expose replacement text instead of
1608
+ # silently dropping bytes from the cursor stream.
1609
+ text = raw.decode("utf-8", errors="replace")
1610
+ decode_status = "LOG_DECODE_DEGRADED"
1611
+ break
1612
+ if (
1613
+ receipt.get("status") in RUN_TERMINAL_STATUSES
1614
+ and cursor + len(raw) >= size
1615
+ ):
1616
+ # Once the producer is terminal, an incomplete trailing
1617
+ # sequence cannot be completed by a later write. Preserve
1618
+ # and consume it with an explicit degraded status.
1093
1619
  text = raw.decode("utf-8", errors="replace")
1620
+ decode_status = "LOG_DECODE_DEGRADED"
1094
1621
  break
1622
+ # A live writer may have split a multibyte character across
1623
+ # this read. Keep the incomplete suffix for the next cursor
1624
+ # request so callers never see duplicate replacement glyphs.
1095
1625
  raw = raw[: exc.start]
1096
1626
  else:
1097
1627
  text = ""
@@ -1108,6 +1638,8 @@ def read_run_session_log(
1108
1638
  "text": text,
1109
1639
  "eof": terminal and next_cursor >= size,
1110
1640
  "status": receipt.get("status"),
1641
+ "rawDigest": "sha256:" + hashlib.sha256(raw).hexdigest(),
1642
+ "decodeStatus": decode_status,
1111
1643
  }
1112
1644
 
1113
1645
 
@@ -1117,28 +1649,10 @@ def cancel_run_session(
1117
1649
  *,
1118
1650
  reason: str = "CANCEL_REQUESTED",
1119
1651
  ) -> dict[str, Any]:
1120
- from harness_service import verify_process_identity
1121
-
1122
1652
  receipt = _load_run_receipt(state_root, session_id)
1123
1653
  if receipt.get("status") in RUN_TERMINAL_STATUSES:
1124
1654
  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:
1655
+ if _receipt_identity_state(receipt) is not True:
1142
1656
  return {
1143
1657
  **receipt,
1144
1658
  "ok": False,