@idleware/ringi 0.5.0 → 0.6.0

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 (378) hide show
  1. package/package.json +1 -1
  2. package/target/dts/adapters/fs/index.d.ts +2 -0
  3. package/target/dts/adapters/fs/lock.d.ts +1 -0
  4. package/target/dts/adapters/fs/write.d.ts +5 -0
  5. package/target/dts/adapters/host-posix/group-state.d.ts +7 -0
  6. package/target/dts/adapters/host-posix/groups.d.ts +1 -0
  7. package/target/dts/adapters/host-posix/index.d.ts +2 -0
  8. package/target/dts/adapters/host-posix/launch.d.ts +7 -0
  9. package/target/dts/adapters/host-posix/probes.d.ts +5 -0
  10. package/target/dts/adapters/host-posix/stats.d.ts +9 -0
  11. package/target/dts/adapters/keys-file/index.d.ts +2 -0
  12. package/target/dts/adapters/state-fs/index.d.ts +5 -0
  13. package/target/dts/adapters/tool-claude/index.d.ts +2 -0
  14. package/target/dts/adapters/tool-codex/index.d.ts +2 -0
  15. package/target/dts/adapters/tool-cursor/index.d.ts +2 -0
  16. package/target/dts/adapters/tool-traces/index.d.ts +3 -0
  17. package/target/dts/adapters/transport-git/branches.d.ts +43 -0
  18. package/target/dts/adapters/transport-git/files.d.ts +2 -0
  19. package/target/dts/adapters/transport-git/index.d.ts +32 -0
  20. package/target/dts/adapters/transport-git/marks.d.ts +20 -0
  21. package/target/dts/adapters/transport-git/remote/admit.d.ts +7 -0
  22. package/target/dts/adapters/transport-git/remote/alarms.d.ts +3 -0
  23. package/target/dts/adapters/transport-git/remote/bootstrap.d.ts +7 -0
  24. package/target/dts/adapters/transport-git/remote/branch-commit.d.ts +3 -0
  25. package/target/dts/adapters/transport-git/remote/branch-control.d.ts +15 -0
  26. package/target/dts/adapters/transport-git/remote/branch-scan.d.ts +2 -0
  27. package/target/dts/adapters/transport-git/remote/branch.d.ts +5 -0
  28. package/target/dts/adapters/transport-git/remote/cache.d.ts +3 -0
  29. package/target/dts/adapters/transport-git/remote/commits.d.ts +2 -0
  30. package/target/dts/adapters/transport-git/remote/forget.d.ts +2 -0
  31. package/target/dts/adapters/transport-git/remote/git.d.ts +26 -0
  32. package/target/dts/adapters/transport-git/remote/heartbeat-facts.d.ts +1 -0
  33. package/target/dts/adapters/transport-git/remote/heartbeat-name.d.ts +6 -0
  34. package/target/dts/adapters/transport-git/remote/heartbeat-object.d.ts +10 -0
  35. package/target/dts/adapters/transport-git/remote/heartbeat-push.d.ts +2 -0
  36. package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +7 -0
  37. package/target/dts/adapters/transport-git/remote/keys.d.ts +13 -0
  38. package/target/dts/adapters/transport-git/remote/main.d.ts +11 -0
  39. package/target/dts/adapters/transport-git/remote/process.d.ts +20 -0
  40. package/target/dts/adapters/transport-git/remote/push-retry.d.ts +2 -0
  41. package/target/dts/adapters/transport-git/remote/push-validate.d.ts +2 -0
  42. package/target/dts/adapters/transport-git/remote/push.d.ts +16 -0
  43. package/target/dts/adapters/transport-git/remote/roster.d.ts +7 -0
  44. package/target/dts/adapters/transport-git/remote/signals.d.ts +15 -0
  45. package/target/dts/adapters/transport-git/remote/signature.d.ts +5 -0
  46. package/target/dts/adapters/transport-git/remote/sync-heartbeat.d.ts +2 -0
  47. package/target/dts/adapters/transport-git/remote/sync-rooms.d.ts +6 -0
  48. package/target/dts/adapters/transport-git/remote/sync-tags.d.ts +8 -0
  49. package/target/dts/adapters/transport-git/remote/sync.d.ts +2 -0
  50. package/target/dts/adapters/transport-git/remote/types.d.ts +64 -0
  51. package/target/dts/adapters/transport-git/remote.d.ts +17 -0
  52. package/target/dts/adapters/transport-git/repo.d.ts +31 -0
  53. package/target/dts/adapters/transport-git/run.d.ts +21 -0
  54. package/target/dts/adapters/transport-git/signing.d.ts +4 -0
  55. package/target/dts/adapters/transport-git/tags.d.ts +15 -0
  56. package/target/dts/adapters/transport-git/transport.d.ts +7 -0
  57. package/target/dts/adapters/transport-kafka/index.d.ts +2 -0
  58. package/target/dts/adapters/transport-temporal/index.d.ts +2 -0
  59. package/target/dts/cli.d.ts +2 -1
  60. package/target/dts/{cli/agent.d.ts → clients/cli/agent/index.d.ts} +3 -3
  61. package/target/dts/{cli → clients/cli/agent}/session.d.ts +2 -2
  62. package/target/dts/clients/cli/arg-parse.d.ts +2 -0
  63. package/target/dts/{cli → clients/cli}/args.d.ts +2 -2
  64. package/target/dts/clients/cli/commands.d.ts +2 -0
  65. package/target/dts/clients/cli/context.d.ts +4 -0
  66. package/target/dts/{cli/index.d.ts → clients/cli/help.d.ts} +1 -3
  67. package/target/dts/clients/cli/index.d.ts +4 -0
  68. package/target/dts/clients/cli/operator/heartbeat-format.d.ts +5 -0
  69. package/target/dts/clients/cli/operator/heartbeat.d.ts +3 -0
  70. package/target/dts/clients/cli/operator/hub-commands.d.ts +6 -0
  71. package/target/dts/clients/cli/operator/hub.d.ts +2 -0
  72. package/target/dts/clients/cli/operator/index.d.ts +5 -0
  73. package/target/dts/clients/cli/operator/room-lifecycle.d.ts +5 -0
  74. package/target/dts/clients/cli/operator/room-list.d.ts +5 -0
  75. package/target/dts/clients/cli/operator/room-members.d.ts +4 -0
  76. package/target/dts/clients/cli/operator/room.d.ts +2 -0
  77. package/target/dts/clients/cli/operator/sessions.d.ts +5 -0
  78. package/target/dts/clients/cli/operator/watch.d.ts +2 -0
  79. package/target/dts/clients/cli/operator/where-format.d.ts +7 -0
  80. package/target/dts/clients/cli/operator/where.d.ts +2 -0
  81. package/target/dts/clients/cli/shared/budget.d.ts +7 -0
  82. package/target/dts/clients/cli/shared/files.d.ts +4 -0
  83. package/target/dts/clients/cli/shared/index.d.ts +7 -0
  84. package/target/dts/clients/cli/shared/messages.d.ts +7 -0
  85. package/target/dts/clients/cli/shared/profile.d.ts +4 -0
  86. package/target/dts/clients/cli/shared/session.d.ts +6 -0
  87. package/target/dts/clients/cli/shared/signals.d.ts +3 -0
  88. package/target/dts/clients/cli/shared/work.d.ts +6 -0
  89. package/target/dts/clients/cli/skill/index.d.ts +3 -0
  90. package/target/dts/{cli/skill.d.ts → clients/cli/skill/targets.d.ts} +1 -2
  91. package/target/dts/composition/index.d.ts +2 -0
  92. package/target/dts/{heartbeat → core/agents/heartbeat}/index.d.ts +2 -4
  93. package/target/dts/{app/heartbeat.d.ts → core/agents/heartbeat/pulse.d.ts} +5 -6
  94. package/target/dts/core/agents/heartbeat/singleton.d.ts +3 -0
  95. package/target/dts/core/agents/heartbeat/tools.d.ts +6 -0
  96. package/target/dts/{app → core/agents}/overview.d.ts +12 -6
  97. package/target/dts/core/agents/ports/host.d.ts +47 -0
  98. package/target/dts/core/agents/ports/index.d.ts +9 -0
  99. package/target/dts/core/agents/ports/tools.d.ts +18 -0
  100. package/target/dts/core/agents/sessions/agent.d.ts +70 -0
  101. package/target/dts/core/agents/sessions/binding.d.ts +9 -0
  102. package/target/dts/core/agents/sessions/context.d.ts +8 -0
  103. package/target/dts/{runtime/session.d.ts → core/agents/sessions/index.d.ts} +2 -9
  104. package/target/dts/core/agents/sessions/join.d.ts +20 -0
  105. package/target/dts/core/agents/sessions/resume.d.ts +2 -0
  106. package/target/dts/core/agents/sessions/session.d.ts +46 -0
  107. package/target/dts/core/agents/sessions/wait.d.ts +38 -0
  108. package/target/dts/core/agents/turns/budgets.d.ts +5 -0
  109. package/target/dts/core/agents/turns/dispatch.d.ts +9 -0
  110. package/target/dts/core/agents/turns/exhaustion.d.ts +11 -0
  111. package/target/dts/core/agents/turns/grants.d.ts +4 -0
  112. package/target/dts/core/agents/turns/index.d.ts +4 -0
  113. package/target/dts/core/agents/turns/limits.d.ts +2 -0
  114. package/target/dts/core/agents/turns/messages.d.ts +10 -0
  115. package/target/dts/core/agents/turns/pings.d.ts +5 -0
  116. package/target/dts/core/agents/turns/plan.d.ts +3 -0
  117. package/target/dts/core/agents/turns/poll.d.ts +8 -0
  118. package/target/dts/core/agents/turns/results.d.ts +3 -0
  119. package/target/dts/core/agents/turns/seat.d.ts +2 -0
  120. package/target/dts/core/agents/turns/types.d.ts +59 -0
  121. package/target/dts/core/hub/decisions/budget.d.ts +15 -0
  122. package/target/dts/core/hub/decisions/check.d.ts +6 -0
  123. package/target/dts/core/hub/decisions/index.d.ts +5 -0
  124. package/target/dts/core/hub/decisions/proposal.d.ts +38 -0
  125. package/target/dts/core/hub/decisions/usage.d.ts +12 -0
  126. package/target/dts/{runtime/reader.d.ts → core/hub/history/index.d.ts} +12 -6
  127. package/target/dts/core/hub/ports/index.d.ts +16 -0
  128. package/target/dts/core/hub/ports/state.d.ts +12 -0
  129. package/target/dts/{store/index.d.ts → core/hub/ports/store.d.ts} +48 -18
  130. package/target/dts/core/hub/protocol/index.d.ts +5 -0
  131. package/target/dts/{protocol/index.d.ts → core/hub/protocol/model.d.ts} +0 -25
  132. package/target/dts/core/hub/protocol/order.d.ts +2 -0
  133. package/target/dts/core/hub/protocol/reading.d.ts +8 -0
  134. package/target/dts/core/hub/protocol/text.d.ts +11 -0
  135. package/target/dts/core/hub/protocol/threads.d.ts +5 -0
  136. package/target/dts/core/hub/rooms/compat.d.ts +5 -0
  137. package/target/dts/core/hub/rooms/hubs.d.ts +14 -0
  138. package/target/dts/core/hub/rooms/index.d.ts +53 -0
  139. package/target/dts/core/hub/rooms/messages.d.ts +8 -0
  140. package/target/dts/core/hub/rooms/rooms.d.ts +10 -0
  141. package/target/dts/core/hub/signals/check.d.ts +3 -0
  142. package/target/dts/core/hub/signals/effective.d.ts +3 -0
  143. package/target/dts/core/hub/signals/index.d.ts +5 -0
  144. package/target/dts/core/hub/signals/marks.d.ts +4 -0
  145. package/target/dts/core/hub/signals/model.d.ts +28 -0
  146. package/target/dts/core/hub/signals/pins.d.ts +5 -0
  147. package/target/dts/{hub/signals.d.ts → core/hub/signals/write.d.ts} +1 -1
  148. package/target/dts/core/hub/usecases/actor.d.ts +6 -0
  149. package/target/dts/{app → core/hub/usecases}/budget.d.ts +5 -18
  150. package/target/dts/core/hub/usecases/config.d.ts +6 -0
  151. package/target/dts/{app → core/hub/usecases}/context.d.ts +3 -8
  152. package/target/dts/{app → core/hub/usecases}/hubs.d.ts +1 -1
  153. package/target/dts/core/hub/usecases/propose.d.ts +17 -0
  154. package/target/dts/{app → core/hub/usecases}/room.d.ts +8 -34
  155. package/target/dts/{app → core/hub/usecases}/rooms.d.ts +2 -6
  156. package/target/dts/core/hub/usecases/roster.d.ts +5 -0
  157. package/target/dts/core/hub/usecases/send.d.ts +21 -0
  158. package/target/dts/core/hub/usecases/staging.d.ts +8 -0
  159. package/target/dts/{app → core/hub/usecases}/view.d.ts +4 -4
  160. package/target/dts/{domain/profiles.d.ts → core/hub/work/index.d.ts} +3 -7
  161. package/target/dts/core/hub/work/parse.d.ts +2 -0
  162. package/target/dts/core/hub/work/task.d.ts +6 -0
  163. package/target/dts/core/signing/index.d.ts +35 -0
  164. package/target/dts/index.d.ts +8 -8
  165. package/target/dts/lib/fail.d.ts +8 -0
  166. package/target/dts/lib/index.d.ts +3 -11
  167. package/target/dts/lib/json.d.ts +1 -0
  168. package/target/esm/adapters/fs/index.js +5 -0
  169. package/target/esm/adapters/fs/lock.js +83 -0
  170. package/target/esm/adapters/fs/write.js +25 -0
  171. package/target/esm/adapters/host-posix/group-state.js +24 -0
  172. package/target/esm/adapters/host-posix/groups.js +43 -0
  173. package/target/esm/adapters/host-posix/index.js +35 -0
  174. package/target/esm/adapters/host-posix/launch.js +80 -0
  175. package/target/esm/adapters/host-posix/probes.js +66 -0
  176. package/target/esm/adapters/host-posix/stats.js +27 -0
  177. package/target/esm/adapters/keys-file/index.js +68 -0
  178. package/target/esm/adapters/state-fs/index.js +79 -0
  179. package/target/esm/adapters/tool-claude/index.js +32 -0
  180. package/target/esm/adapters/tool-codex/index.js +38 -0
  181. package/target/esm/adapters/tool-cursor/index.js +8 -0
  182. package/target/esm/adapters/tool-traces/index.js +18 -0
  183. package/target/esm/adapters/transport-git/branches.js +155 -0
  184. package/target/esm/adapters/transport-git/files.js +48 -0
  185. package/target/esm/adapters/transport-git/index.js +128 -0
  186. package/target/esm/adapters/transport-git/marks.js +88 -0
  187. package/target/esm/adapters/transport-git/remote/admit.js +71 -0
  188. package/target/esm/adapters/transport-git/remote/alarms.js +30 -0
  189. package/target/esm/adapters/transport-git/remote/bootstrap.js +47 -0
  190. package/target/esm/adapters/transport-git/remote/branch-commit.js +36 -0
  191. package/target/esm/adapters/transport-git/remote/branch-control.js +37 -0
  192. package/target/esm/adapters/transport-git/remote/branch-scan.js +28 -0
  193. package/target/esm/adapters/transport-git/remote/branch.js +37 -0
  194. package/target/esm/adapters/transport-git/remote/cache.js +23 -0
  195. package/target/esm/adapters/transport-git/remote/commits.js +25 -0
  196. package/target/esm/adapters/transport-git/remote/forget.js +35 -0
  197. package/target/esm/adapters/transport-git/remote/git.js +58 -0
  198. package/target/esm/adapters/transport-git/remote/heartbeat-facts.js +35 -0
  199. package/target/esm/adapters/transport-git/remote/heartbeat-name.js +19 -0
  200. package/target/esm/adapters/transport-git/remote/heartbeat-object.js +40 -0
  201. package/target/esm/adapters/transport-git/remote/heartbeat-push.js +44 -0
  202. package/target/esm/adapters/transport-git/remote/heartbeat.js +44 -0
  203. package/target/esm/adapters/transport-git/remote/keys.js +75 -0
  204. package/target/esm/adapters/transport-git/remote/main.js +41 -0
  205. package/target/esm/adapters/transport-git/remote/process.js +37 -0
  206. package/target/esm/adapters/transport-git/remote/push-retry.js +46 -0
  207. package/target/esm/adapters/transport-git/remote/push-validate.js +39 -0
  208. package/target/esm/adapters/transport-git/remote/push.js +62 -0
  209. package/target/esm/adapters/transport-git/remote/roster.js +48 -0
  210. package/target/esm/adapters/transport-git/remote/signals.js +63 -0
  211. package/target/esm/adapters/transport-git/remote/signature.js +56 -0
  212. package/target/esm/adapters/transport-git/remote/sync-heartbeat.js +26 -0
  213. package/target/esm/adapters/transport-git/remote/sync-rooms.js +38 -0
  214. package/target/esm/adapters/transport-git/remote/sync-tags.js +48 -0
  215. package/target/esm/adapters/transport-git/remote/sync.js +38 -0
  216. package/target/esm/adapters/transport-git/remote/types.js +10 -0
  217. package/target/esm/adapters/transport-git/remote.js +17 -0
  218. package/target/esm/adapters/transport-git/repo.js +115 -0
  219. package/target/esm/adapters/transport-git/run.js +57 -0
  220. package/target/esm/adapters/transport-git/signing.js +44 -0
  221. package/target/esm/adapters/transport-git/tags.js +21 -0
  222. package/target/esm/adapters/transport-git/transport.js +29 -0
  223. package/target/esm/adapters/transport-kafka/index.js +4 -0
  224. package/target/esm/adapters/transport-temporal/index.js +4 -0
  225. package/target/esm/cli.js +5 -4
  226. package/target/esm/{cli/agent.js → clients/cli/agent/index.js} +8 -7
  227. package/target/esm/{cli → clients/cli/agent}/session.js +7 -7
  228. package/target/esm/clients/cli/arg-parse.js +41 -0
  229. package/target/esm/{cli → clients/cli}/args.js +3 -36
  230. package/target/esm/clients/cli/commands.js +45 -0
  231. package/target/esm/clients/cli/context.js +6 -0
  232. package/target/esm/{cli/index.js → clients/cli/help.js} +4 -77
  233. package/target/esm/clients/cli/index.js +27 -0
  234. package/target/esm/clients/cli/operator/heartbeat-format.js +8 -0
  235. package/target/esm/clients/cli/operator/heartbeat.js +35 -0
  236. package/target/esm/clients/cli/operator/hub-commands.js +37 -0
  237. package/target/esm/clients/cli/operator/hub.js +10 -0
  238. package/target/esm/clients/cli/operator/index.js +8 -0
  239. package/target/esm/clients/cli/operator/room-lifecycle.js +23 -0
  240. package/target/esm/clients/cli/operator/room-list.js +27 -0
  241. package/target/esm/clients/cli/operator/room-members.js +15 -0
  242. package/target/esm/clients/cli/operator/room.js +15 -0
  243. package/target/esm/clients/cli/operator/sessions.js +32 -0
  244. package/target/esm/clients/cli/operator/watch.js +15 -0
  245. package/target/esm/clients/cli/operator/where-format.js +24 -0
  246. package/target/esm/clients/cli/operator/where.js +32 -0
  247. package/target/esm/clients/cli/shared/budget.js +50 -0
  248. package/target/esm/clients/cli/shared/files.js +26 -0
  249. package/target/esm/clients/cli/shared/index.js +10 -0
  250. package/target/esm/clients/cli/shared/messages.js +41 -0
  251. package/target/esm/clients/cli/shared/profile.js +17 -0
  252. package/target/esm/clients/cli/shared/session.js +8 -0
  253. package/target/esm/clients/cli/shared/signals.js +21 -0
  254. package/target/esm/clients/cli/shared/work.js +26 -0
  255. package/target/esm/clients/cli/skill/index.js +40 -0
  256. package/target/esm/clients/cli/skill/targets.js +20 -0
  257. package/target/esm/composition/index.js +34 -0
  258. package/target/esm/{heartbeat → core/agents/heartbeat}/index.js +14 -12
  259. package/target/esm/{app/heartbeat.js → core/agents/heartbeat/pulse.js} +31 -55
  260. package/target/esm/core/agents/heartbeat/singleton.js +38 -0
  261. package/target/esm/core/agents/heartbeat/tools.js +24 -0
  262. package/target/esm/{app → core/agents}/overview.js +15 -9
  263. package/target/esm/core/agents/ports/host.js +4 -0
  264. package/target/esm/core/agents/ports/index.js +7 -0
  265. package/target/esm/core/agents/ports/tools.js +5 -0
  266. package/target/esm/{app → core/agents/sessions}/agent.js +11 -43
  267. package/target/esm/core/agents/sessions/binding.js +43 -0
  268. package/target/esm/core/agents/sessions/context.js +11 -0
  269. package/target/esm/core/agents/sessions/index.js +59 -0
  270. package/target/esm/core/agents/sessions/join.js +55 -0
  271. package/target/esm/core/agents/sessions/resume.js +54 -0
  272. package/target/esm/core/agents/sessions/session.js +28 -0
  273. package/target/esm/core/agents/sessions/wait.js +47 -0
  274. package/target/esm/core/agents/turns/budgets.js +48 -0
  275. package/target/esm/core/agents/turns/dispatch.js +40 -0
  276. package/target/esm/core/agents/turns/exhaustion.js +16 -0
  277. package/target/esm/core/agents/turns/grants.js +36 -0
  278. package/target/esm/core/agents/turns/index.js +33 -0
  279. package/target/esm/core/agents/turns/limits.js +11 -0
  280. package/target/esm/core/agents/turns/messages.js +32 -0
  281. package/target/esm/core/agents/turns/pings.js +30 -0
  282. package/target/esm/core/agents/turns/plan.js +29 -0
  283. package/target/esm/core/agents/turns/poll.js +48 -0
  284. package/target/esm/core/agents/turns/results.js +40 -0
  285. package/target/esm/core/agents/turns/seat.js +44 -0
  286. package/target/esm/core/agents/turns/types.js +3 -0
  287. package/target/esm/{domain/index.js → core/hub/capabilities.js} +3 -3
  288. package/target/esm/core/hub/decisions/budget.js +40 -0
  289. package/target/esm/core/hub/decisions/check.js +43 -0
  290. package/target/esm/core/hub/decisions/index.js +10 -0
  291. package/target/esm/core/hub/decisions/proposal.js +73 -0
  292. package/target/esm/core/hub/decisions/usage.js +39 -0
  293. package/target/esm/core/hub/history/index.js +90 -0
  294. package/target/esm/core/hub/ports/index.js +10 -0
  295. package/target/esm/core/hub/ports/state.js +5 -0
  296. package/target/esm/core/hub/ports/store.js +10 -0
  297. package/target/esm/{protocol → core/hub/protocol}/files.js +4 -2
  298. package/target/esm/core/hub/protocol/index.js +9 -0
  299. package/target/esm/core/hub/protocol/model.js +15 -0
  300. package/target/esm/core/hub/protocol/order.js +39 -0
  301. package/target/esm/core/hub/protocol/reading.js +29 -0
  302. package/target/esm/core/hub/protocol/text.js +95 -0
  303. package/target/esm/core/hub/protocol/threads.js +52 -0
  304. package/target/esm/core/hub/rooms/compat.js +42 -0
  305. package/target/esm/core/hub/rooms/hubs.js +92 -0
  306. package/target/esm/core/hub/rooms/index.js +23 -0
  307. package/target/esm/core/hub/rooms/messages.js +47 -0
  308. package/target/esm/core/hub/rooms/rooms.js +84 -0
  309. package/target/esm/core/hub/signals/check.js +33 -0
  310. package/target/esm/core/hub/signals/effective.js +31 -0
  311. package/target/esm/core/hub/signals/index.js +23 -0
  312. package/target/esm/core/hub/signals/marks.js +30 -0
  313. package/target/esm/core/hub/signals/model.js +5 -0
  314. package/target/esm/core/hub/signals/pins.js +21 -0
  315. package/target/esm/{hub/signals.js → core/hub/signals/write.js} +4 -4
  316. package/target/esm/core/hub/usecases/actor.js +3 -0
  317. package/target/esm/core/hub/usecases/budget.js +78 -0
  318. package/target/esm/core/hub/usecases/config.js +5 -0
  319. package/target/esm/core/hub/usecases/context.js +13 -0
  320. package/target/esm/core/hub/usecases/hubs.js +95 -0
  321. package/target/esm/core/hub/usecases/propose.js +39 -0
  322. package/target/esm/core/hub/usecases/room.js +82 -0
  323. package/target/esm/{app → core/hub/usecases}/rooms.js +11 -63
  324. package/target/esm/core/hub/usecases/roster.js +47 -0
  325. package/target/esm/core/hub/usecases/send.js +78 -0
  326. package/target/esm/core/hub/usecases/staging.js +60 -0
  327. package/target/esm/{app → core/hub/usecases}/view.js +33 -33
  328. package/target/esm/core/hub/work/index.js +13 -0
  329. package/target/esm/core/hub/work/parse.js +37 -0
  330. package/target/esm/core/hub/work/task.js +68 -0
  331. package/target/esm/core/signing/index.js +4 -0
  332. package/target/esm/index.js +7 -6
  333. package/target/esm/lib/fail.js +18 -0
  334. package/target/esm/lib/index.js +20 -195
  335. package/target/esm/lib/json.js +81 -0
  336. package/target/dts/app/agent.d.ts +0 -109
  337. package/target/dts/app/session.d.ts +0 -65
  338. package/target/dts/cli/context.d.ts +0 -6
  339. package/target/dts/cli/operator.d.ts +0 -10
  340. package/target/dts/cli/shared.d.ts +0 -27
  341. package/target/dts/domain/decisions.d.ts +0 -65
  342. package/target/dts/domain/signals.d.ts +0 -37
  343. package/target/dts/heartbeat/platform.d.ts +0 -15
  344. package/target/dts/heartbeat/tools.d.ts +0 -11
  345. package/target/dts/home/index.d.ts +0 -21
  346. package/target/dts/hub/index.d.ts +0 -72
  347. package/target/dts/runtime/watch.d.ts +0 -17
  348. package/target/dts/store/git/index.d.ts +0 -92
  349. package/target/dts/store/git/remote.d.ts +0 -95
  350. package/target/dts/store/kafka/index.d.ts +0 -2
  351. package/target/dts/store/temporal/index.d.ts +0 -2
  352. package/target/esm/app/budget.js +0 -95
  353. package/target/esm/app/context.js +0 -16
  354. package/target/esm/app/hubs.js +0 -76
  355. package/target/esm/app/room.js +0 -181
  356. package/target/esm/app/session.js +0 -99
  357. package/target/esm/cli/context.js +0 -8
  358. package/target/esm/cli/operator.js +0 -185
  359. package/target/esm/cli/shared.js +0 -159
  360. package/target/esm/cli/skill.js +0 -54
  361. package/target/esm/domain/decisions.js +0 -178
  362. package/target/esm/domain/profiles.js +0 -96
  363. package/target/esm/domain/signals.js +0 -134
  364. package/target/esm/heartbeat/platform.js +0 -83
  365. package/target/esm/heartbeat/tools.js +0 -68
  366. package/target/esm/home/index.js +0 -88
  367. package/target/esm/hub/index.js +0 -224
  368. package/target/esm/protocol/index.js +0 -219
  369. package/target/esm/runtime/reader.js +0 -65
  370. package/target/esm/runtime/session.js +0 -116
  371. package/target/esm/runtime/watch.js +0 -321
  372. package/target/esm/store/git/index.js +0 -719
  373. package/target/esm/store/git/remote.js +0 -928
  374. package/target/esm/store/index.js +0 -15
  375. package/target/esm/store/kafka/index.js +0 -3
  376. package/target/esm/store/temporal/index.js +0 -3
  377. /package/target/dts/{domain/index.d.ts → core/hub/capabilities.d.ts} +0 -0
  378. /package/target/dts/{protocol → core/hub/protocol}/files.d.ts +0 -0
@@ -1,928 +0,0 @@
1
- // Signed remote hubs for ringi. The remote is an untrusted replica, never a trust anchor.
2
- // Fetch into staging refs, verify against an out-of-band root pin and a root-signed room roster,
3
- // then advance local/accepted refs. Private keys stay under this machine's RINGI_HOME.
4
- import fs from 'node:fs';
5
- import os from 'node:os';
6
- import path from 'node:path';
7
- import crypto from 'node:crypto';
8
- import { spawnSync } from 'node:child_process';
9
- import * as C from '../../lib/index.js';
10
- import * as P from '../../protocol/index.js';
11
- const CONTROL_FILES = ['ROOM.md', 'members.json', 'profile.json', 'allowed_signers'];
12
- const reason = (e) => e instanceof Error ? e.message : String(e);
13
- const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
14
- const ROOT = 'operator';
15
- const TRACK = 'refs/remotes/origin/';
16
- const ACCEPT = 'refs/ringi/remote-accepted/';
17
- const TAGS = 'refs/tags/';
18
- const TAG_TRACK = 'refs/ringi/remote-tags/';
19
- const TAG_ACCEPT = 'refs/ringi/remote-accepted-tags/';
20
- const ENV = { GIT_CONFIG_GLOBAL: os.devNull, GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0', GIT_NO_REPLACE_OBJECTS: '1', LC_ALL: 'C' };
21
- const BASE = ['-c', 'core.hooksPath=' + os.devNull, '-c', 'gpg.format=ssh', '-c', 'gpg.ssh.program=ssh-keygen',
22
- '-c', 'color.ui=false', '-c', 'log.showSignature=false', '-c', 'core.quotePath=false',
23
- '-c', 'protocol.allow=never', '-c', 'protocol.file.allow=always', '-c', 'protocol.ssh.allow=always',
24
- '-c', 'protocol.https.allow=always', '-c', 'http.followRedirects=false'];
25
- const verified = new Set();
26
- // Reuse immutable-head checks within one operation; no ref or alarm survives into the next one.
27
- const checking = new WeakMap();
28
- export const withVerification = (H, fn) => {
29
- if (checking.has(H))
30
- return fn();
31
- checking.set(H, new Map());
32
- try {
33
- return fn();
34
- }
35
- finally {
36
- checking.delete(H);
37
- }
38
- };
39
- const memo = (H, key, fn) => {
40
- const cache = checking.get(H);
41
- if (cache?.has(key))
42
- return cache.get(key);
43
- const value = fn();
44
- cache?.set(key, value);
45
- return value;
46
- };
47
- const { note, strictJson } = C;
48
- export { strictJson };
49
- const mkdir = (d) => fs.mkdirSync(d, { recursive: true, mode: 0o700 });
50
- const id = (s) => P.ID_RE.test(s) ? s : C.fail(`bad agent or hub id ${s}`);
51
- const zero = (H) => '0'.repeat(git(H, ['rev-parse', '--show-object-format']).trim() === 'sha256' ? 64 : 40);
52
- const ident = (who) => ({ GIT_AUTHOR_NAME: who, GIT_AUTHOR_EMAIL: `${who}@ringi`, GIT_COMMITTER_NAME: who, GIT_COMMITTER_EMAIL: `${who}@ringi` });
53
- export const gitEnv = (extra = {}) => ({
54
- ...Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^(GIT_CONFIG_|GIT_DIR$|GIT_WORK_TREE$|GIT_INDEX_FILE$|GIT_OBJECT_DIRECTORY$|GIT_ALTERNATE_OBJECT_DIRECTORIES$|GIT_REPLACE_REF_BASE$)/.test(k))),
55
- ...ENV, ...extra,
56
- });
57
- export const gitTimeout = () => {
58
- const ms = Number(process.env.RINGI_GIT_TIMEOUT_MS ?? 30000);
59
- if (!Number.isSafeInteger(ms) || ms < 1)
60
- C.fail('RINGI_GIT_TIMEOUT_MS must be a positive integer (milliseconds)');
61
- return ms;
62
- };
63
- export const checkGitTimeout = (error) => {
64
- if (error?.code === 'ETIMEDOUT')
65
- throw Object.assign(new C.Fail('git timed out; increase RINGI_GIT_TIMEOUT_MS (positive integer milliseconds)'), { code: 'ETIMEDOUT' });
66
- };
67
- function command(cmd, args, opt = {}) {
68
- const r = spawnSync(cmd, args, { input: opt.input, encoding: opt.raw ? 'buffer' : 'utf8', env: gitEnv(opt.env),
69
- maxBuffer: 64 * 1024 * 1024, timeout: cmd === 'git' ? gitTimeout() : 30000 });
70
- if (cmd === 'git')
71
- checkGitTimeout(r.error);
72
- if (r.error && !opt.ok)
73
- C.fail(`${cmd}: ${r.error.message}`);
74
- if (r.status !== 0 && !opt.ok)
75
- C.fail(`${cmd}: ${String(r.stderr || r.error?.message || r.signal || 'failed').trim().split('\n').at(-1)}`);
76
- return opt.ok ? r : r.stdout;
77
- }
78
- function git(H, args, opt) {
79
- return command('git', ['--git-dir', H.dir, ...BASE, ...args], opt);
80
- }
81
- const config = (H, key, value) => value === undefined
82
- ? String(git(H, ['config', '--get', key], { ok: true }).stdout || '').trim()
83
- : git(H, ['config', key, String(value)]);
84
- const rev = (H, ref) => {
85
- const r = git(H, ['rev-parse', '-q', '--verify', `${ref}^{commit}`], { ok: true });
86
- return r.status === 0 ? r.stdout.trim() : null;
87
- };
88
- const object = (H, ref) => {
89
- const r = git(H, ['rev-parse', '-q', '--verify', ref], { ok: true });
90
- return r.status === 0 ? r.stdout.trim() : null;
91
- };
92
- const ancestor = (H, a, b) => !!a && !!b && git(H, ['merge-base', '--is-ancestor', a, b], { ok: true }).status === 0;
93
- export const isRemote = (H) => {
94
- if (H.d.trust !== 'signed' && H.d.trust !== 'asserted')
95
- C.fail(`unsupported hub trust ${String(H.d.trust)}`);
96
- return H.d.trust === 'signed';
97
- };
98
- const origin = (H) => H.d.store.remote;
99
- const suffix = (ref) => ref.replace(/^refs\/heads\//, '');
100
- const accepted = (ref) => ACCEPT + suffix(ref);
101
- const tracking = (ref) => TRACK + suffix(ref);
102
- // Transport and home state use the same owner-aware lock.
103
- const locked = (H, fn) => {
104
- const file = path.join(H.dir, 'ringi', 'transport.lock');
105
- mkdir(path.dirname(file));
106
- return C.locked(file, () => withVerification(H, fn));
107
- };
108
- export const parseKey = (value) => {
109
- const text = String(value || '').trim();
110
- const m = /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(?:256|384|521)) ([A-Za-z0-9+/]+={0,2})(?: [^\r\n]*)?$/.exec(text) ?? C.fail('expected one SSH public key (or a file containing it)');
111
- if (m[2].length > 16384)
112
- C.fail('expected one SSH public key (or a file containing it)');
113
- const data = Buffer.from(m[2], 'base64');
114
- if (data.length < 8 || data.readUInt32BE(0) > data.length - 4 || data.subarray(4, 4 + data.readUInt32BE(0)).toString() !== m[1])
115
- C.fail('malformed SSH public key');
116
- const line = `${m[1]} ${m[2]}`;
117
- return { line, fp: 'SHA256:' + crypto.createHash('sha256').update(data).digest('base64').replace(/=+$/, '') };
118
- };
119
- const keyInput = (value) => parseKey(fs.existsSync(value) && fs.statSync(value).isFile() ? fs.readFileSync(value, 'utf8') : value);
120
- const rootKey = (H) => parseKey(H.d.root || C.fail(`hub ${H.name} has no out-of-band root pin; use hub add --root <public-key|file>`));
121
- const signerLine = (who, key) => `${who} namespaces="git" ${key.line}\n`;
122
- const keyPath = (H, who, home) => path.join(home, 'keys', id(H.name), id(who));
123
- const localKey = (H, who, home, create = false) => {
124
- const file = keyPath(H, who, home);
125
- if (!fs.existsSync(file)) {
126
- if (!create)
127
- C.fail(`no private key for ${who} on this machine; ${who === ROOT ? 'root keys are never obtained from a hub' : 'join here to generate one, then ask the root to admit its public key'}`);
128
- mkdir(path.dirname(file));
129
- command('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', `${who}@ringi`, '-f', file]);
130
- }
131
- // Derive from the private key instead of trusting a mutable .pub sidecar.
132
- const key = parseKey(command('ssh-keygen', ['-y', '-f', file]));
133
- return { ...key, file };
134
- };
135
- export const setupSigned = (H, home, url) => {
136
- id(H.name);
137
- if (rev(H, 'refs/heads/main'))
138
- C.fail('setupSigned only initializes an empty hub; it cannot replace an existing trust anchor');
139
- const key = localKey(H, ROOT, home, true);
140
- H.d.trust = 'signed';
141
- H.d.root = key.line;
142
- config(H, 'gpg.format', 'ssh');
143
- config(H, 'gpg.ssh.program', 'ssh-keygen');
144
- if (url) {
145
- checkUrl(url);
146
- H.d.store.remote = url;
147
- }
148
- return { root: key.line, allowedSigners: signerLine(ROOT, key) };
149
- };
150
- export const sign = (H, who, home) => {
151
- if (!isRemote(H))
152
- return [];
153
- const key = localKey(H, who, home);
154
- if (who === ROOT && key.line !== rootKey(H).line)
155
- C.fail('the local operator key does not match this hub\'s pinned root');
156
- config(H, 'gpg.format', 'ssh');
157
- config(H, 'gpg.ssh.program', 'ssh-keygen');
158
- return [`-S${key.file}`];
159
- };
160
- export const rootRoster = (H) => signerLine(ROOT, rootKey(H));
161
- const checkUrl = (url) => {
162
- if (typeof url !== 'string' || /[\r\n\0]/.test(url) || url.startsWith('-') || /^http:\/\//i.test(url) || !/^(?:file|https|ssh):\/\/|^(?:[^\s/@:]+@)?[^\s/:]+:[^\s]+$|^\//.test(url))
163
- C.fail('use a file://, https:// or ssh Git URL; cleartext HTTP and custom transport helpers are refused');
164
- };
165
- const blobText = (H, sha, name) => {
166
- const entry = git(H, ['ls-tree', sha, '--', name]).trim();
167
- if (!/^100(?:644|755) blob [0-9a-f]+\t/.test(entry))
168
- C.fail(`${name} at ${sha.slice(0, 12)} must be a regular blob`);
169
- const raw = git(H, ['cat-file', 'blob', `${sha}:${name}`], { raw: true });
170
- const d = P.decodeText(raw);
171
- if (d.invalid)
172
- C.fail(`${name} at ${sha.slice(0, 12)} is ${d.invalid}`);
173
- return d.text;
174
- };
175
- const rosterAt = (H, sha) => {
176
- const entries = blobText(H, sha, 'allowed_signers').split('\n').map(s => s.trim()).filter(s => s && !s.startsWith('#')).map(line => {
177
- const m = /^(\S+) namespaces="git" (.+)$/.exec(line) ?? C.fail('malformed allowed_signers; only literal principals and git namespace are supported');
178
- if (!P.ID_RE.test(m[1]))
179
- C.fail('malformed allowed_signers; only literal principals and git namespace are supported');
180
- return { principal: m[1], key: parseKey(m[2]) };
181
- });
182
- const keys = new Set(), principals = new Set();
183
- for (const e of entries) {
184
- if (keys.has(e.key.fp) || principals.has(e.principal))
185
- C.fail('duplicate key or principal in allowed_signers; rotation requires an explicit revocation protocol');
186
- keys.add(e.key.fp);
187
- principals.add(e.principal);
188
- }
189
- const members = strictJson(blobText(H, sha, 'members.json'), 'members.json');
190
- if (!members || typeof members !== 'object' || Array.isArray(members))
191
- C.fail('members.json must be an object');
192
- if (entries.find(e => e.principal === ROOT)?.key.line !== rootKey(H).line || members[ROOT]?.role !== 'root')
193
- C.fail('room roster must retain the pinned operator as root');
194
- if (Object.keys(members).some(who => !P.ID_RE.test(who) || !principals.has(who) || (who !== ROOT && members[who]?.role === 'root')) || entries.some(e => !members[e.principal]))
195
- C.fail('members.json and allowed_signers disagree');
196
- return { entries, members };
197
- };
198
- const signerFile = (H, text) => {
199
- const dir = path.join(H.dir, 'ringi', 'verify');
200
- mkdir(dir);
201
- const file = path.join(dir, crypto.createHash('sha256').update(text).digest('hex') + '.allowed_signers');
202
- if (!fs.existsSync(file))
203
- fs.writeFileSync(file, text, { flag: 'wx', mode: 0o600 });
204
- else if (fs.readFileSync(file, 'utf8') !== text)
205
- C.fail('local signature verification cache was changed');
206
- return file;
207
- };
208
- const checkSignature = (H, sha, who, key, kind = 'commit') => {
209
- const cache = `${kind}/${sha}/${who}/${key.fp}`;
210
- if (verified.has(cache))
211
- return;
212
- const file = signerFile(H, signerLine(who, key));
213
- const r = git(H, ['-c', `gpg.ssh.allowedSignersFile=${file}`, '-c', 'gpg.ssh.revocationFile=' + os.devNull, `verify-${kind}`, sha], { ok: true });
214
- if (r.status !== 0)
215
- C.fail(`bad or missing signature at ${sha.slice(0, 12)}: not signed by the admitted key of ${who}`);
216
- const raw = git(H, ['cat-file', kind, sha], { raw: true });
217
- const split = raw.indexOf(Buffer.from('\n\n'));
218
- if (split < 0)
219
- C.fail(`malformed ${kind}`);
220
- const hdr = raw.subarray(0, split).toString('utf8');
221
- if (/^encoding (?!UTF-8$|utf-8$)/m.test(hdr))
222
- C.fail('commit declares a non-UTF-8 message');
223
- const d = P.decodeText(raw.subarray(split + 2));
224
- if (d.invalid)
225
- C.fail(`${kind} message is ${d.invalid}`);
226
- verified.add(cache);
227
- if (verified.size > 4096)
228
- verified.delete(verified.values().next().value);
229
- };
230
- const chain = (H, head, base) => git(H, ['rev-list', '--parents', '--reverse', '--topo-order', head, ...(base ? [`^${base}`] : [])]).trim().split('\n').filter(Boolean).map(line => {
231
- const [sha, ...parents] = line.split(' ');
232
- return { sha, parents };
233
- });
234
- const linear = (commits, initial, check) => {
235
- let expect = initial;
236
- for (const { sha, parents: ps } of commits) {
237
- if (ps.length > 1 || (ps[0] || null) !== expect)
238
- C.fail(`history is not linear at ${sha.slice(0, 12)}`);
239
- check(sha);
240
- expect = sha;
241
- }
242
- };
243
- const verifyMain = (H, head) => memo(H, `main/${head}`, () => {
244
- const key = rootKey(H);
245
- const commits = chain(H, head);
246
- if (!commits.length)
247
- C.fail('signed hub has no genesis');
248
- const pin = H.d.genesis;
249
- if (!pin && !H.bootstrap)
250
- C.fail(`hub ${H.name} has no pinned genesis`);
251
- if (pin && commits[0].sha !== pin)
252
- C.fail('hub does not descend from the pinned genesis');
253
- linear(commits, null, sha => {
254
- checkSignature(H, sha, ROOT, key);
255
- // The shared contract is contract.json; hubs made before the rename carry it as hub.json.
256
- const name = git(H, ['cat-file', '-e', `${sha}:contract.json`], { ok: true }).status === 0 ? 'contract.json' : 'hub.json';
257
- const meta = strictJson(blobText(H, sha, name), name);
258
- if (meta.schema !== 'ringi-hub' || meta.trust !== 'signed' || parseKey(meta.root).line !== key.line)
259
- C.fail(`${name} must declare signed trust and the pinned root key`);
260
- const authority = git(H, ['cat-file', '-e', `${sha}:authority.json`], { ok: true });
261
- if (authority.status === 0)
262
- strictJson(blobText(H, sha, 'authority.json'), 'authority.json');
263
- });
264
- if (!pin)
265
- H.d.genesis = commits[0].sha;
266
- H.bootstrap = false;
267
- return { head, genesis: commits[0].sha, commits: commits.map(c => c.sha) };
268
- });
269
- export const verifyHub = (H) => {
270
- if (!isRemote(H))
271
- return null;
272
- const head = rev(H, 'refs/heads/main') || C.fail('signed hub has no main');
273
- return verifyMain(H, head);
274
- };
275
- export const readCommits = (H, shas) => {
276
- if (!shas.length)
277
- return [];
278
- const out = git(H, ['cat-file', '--batch'], { input: Buffer.from(shas.join('\n') + '\n'), raw: true });
279
- let at = 0;
280
- return shas.map(sha => {
281
- const nl = out.indexOf(0x0a, at);
282
- const [got, type, size] = out.subarray(at, nl).toString('utf8').split(' ');
283
- if (got !== sha || type !== 'commit')
284
- C.fail(`malformed commit ${sha.slice(0, 12)}`);
285
- const body = out.subarray(nl + 1, nl + 1 + Number(size));
286
- at = nl + 1 + Number(size) + 1;
287
- const split = body.indexOf(Buffer.from('\n\n'));
288
- const hdr = body.subarray(0, split < 0 ? body.length : split).toString('utf8');
289
- const parents = [...hdr.matchAll(/^parent ([0-9a-f]+)$/gm)].map(m => m[1]);
290
- const author = /^author (.*?) <[^>\n]*> \d+ [+-]\d{4}$/m.exec(hdr)?.[1] ?? '';
291
- const message = split < 0 ? Buffer.alloc(0) : body.subarray(split + 2);
292
- return { sha, parents, author, body: message, text: P.decodeText(message).text ?? '' };
293
- });
294
- };
295
- const broken = (why) => Object.assign(new C.Fail(why), { broken: true });
296
- // A room is one branch on the verified contract: the root's genesis, then members' messages and the root's control
297
- // records, along first parents. What breaks the room (its genesis, a root-signed control record, the roster) fails it
298
- // whole; any other commit that is not a valid message of an admitted member is skipped, and the room reads on.
299
- const branchData = (H, name, tip, expectedGenesis) => {
300
- const data = memo(H, `branch/${tip}`, () => {
301
- const main = verifyHub(H) ?? C.fail('room verification requires a signed hub');
302
- const shas = git(H, ['rev-list', '--first-parent', '--reverse', tip, `^${main.head}`]).trim().split('\n').filter(Boolean);
303
- if (!shas.length)
304
- C.fail(`room ${name} has no genesis`);
305
- const list = readCommits(H, shas);
306
- const genesis = list[0];
307
- if (genesis.parents.length !== 1 || !main.commits.includes(genesis.parents[0]))
308
- C.fail('room does not descend from the verified hub contract');
309
- const touching = new Set(git(H, ['log', '--first-parent', '--format=%H', tip, `^${main.head}`, '--', ...CONTROL_FILES]).trim().split('\n').filter(Boolean));
310
- let roster = { entries: [], members: {} };
311
- const control = [], authors = new Map(), skipped = new Map();
312
- let parent = genesis.parents[0];
313
- for (const c of list) {
314
- if (c.parents.length !== 1 || c.parents[0] !== parent)
315
- throw broken('a merge or non-linear history; a room is one linear branch');
316
- parent = c.sha;
317
- try {
318
- if (c === genesis || (c.author === ROOT && P.parse(c.text, x => SHA.test(x)).kind === 'room')) {
319
- try {
320
- checkSignature(H, c.sha, ROOT, rootKey(H));
321
- }
322
- catch (e) {
323
- if (c === genesis || C.errno(e) === 'ETIMEDOUT')
324
- throw e;
325
- C.fail(`${reason(e)}; only the root writes control records`);
326
- }
327
- // A control record the root signed is the room's: a broken one breaks the room.
328
- let next;
329
- try {
330
- next = rosterAt(H, c.sha);
331
- }
332
- catch (e) {
333
- throw broken(reason(e));
334
- }
335
- for (const e of roster.entries)
336
- if (!next.entries.some(n => n.principal === e.principal && n.key.line === e.key.line))
337
- throw broken('room roster drops or changes an admitted key; revocation/freeze is not implemented');
338
- roster = next;
339
- control.push(c.sha);
340
- continue;
341
- }
342
- const signer = roster.entries.find(e => e.principal === c.author) ?? C.fail(`${c.author || 'its author'} is unadmitted in this room`);
343
- if (touching.has(c.sha))
344
- C.fail('it changes the room\'s control files, which only the root changes');
345
- checkSignature(H, c.sha, c.author, signer.key);
346
- authors.set(c.sha, c.author);
347
- }
348
- catch (e) {
349
- if (!(e instanceof C.Fail) || c === genesis || e.broken || C.errno(e) === 'ETIMEDOUT')
350
- throw e;
351
- skipped.set(c.sha, reason(e));
352
- }
353
- }
354
- return { genesis: genesis.sha, subject: genesis.text.split('\n')[0], ...roster, control, authors, skipped };
355
- });
356
- // A ref name is supplied by the replica; only the signed genesis names the room.
357
- if (data.subject !== `room ${name}`)
358
- C.fail(`room genesis subject must be "room ${name}"`);
359
- if (expectedGenesis && data.genesis !== expectedGenesis)
360
- C.fail('room changed its genesis');
361
- return data;
362
- };
363
- // The room's branch verified up to `tip`; a quarantined room is not read.
364
- export const branch = (H, R, tip) => {
365
- if (!isRemote(H))
366
- C.fail('room verification requires a signed hub');
367
- if (H.remoteAlarms?.[R.ref])
368
- C.fail(`ALARM ${H.remoteAlarms[R.ref]}; room is quarantined`);
369
- return branchData(H, R.name, tip, R.genesis);
370
- };
371
- // The roster in effect at the room's latest control record.
372
- export const verifyRoom = (H, R) => branch(H, R, R.head);
373
- // Why `head` is not a valid message of `who`, or null.
374
- export const verify = (H, R, who, head) => {
375
- if (!isRemote(H))
376
- return null;
377
- try {
378
- const b = branch(H, R, head);
379
- return b.authors.get(head) === who ? null : b.skipped.get(head) ?? `${head.slice(0, 12)} is not a message of ${who}`;
380
- }
381
- catch (e) {
382
- return reason(e);
383
- }
384
- };
385
- // Heartbeats deliberately share the tag transport, but never the immutable signal acceptance.
386
- // Only this exact namespace is mutable; e.g. `actor.opened.heartbeat.<id>` is still a signal.
387
- const HEARTBEAT_NAME = /^([a-z0-9]+(?:[_-][a-z0-9]+)*)\.heartbeat\.([0-9a-f]{12})$/;
388
- export const heartbeatName = (ref) => {
389
- const m = ref.startsWith(TAGS) && HEARTBEAT_NAME.exec(ref.slice(TAGS.length));
390
- return m ? { actor: m[1], genesis12: m[2] } : null;
391
- };
392
- export const heartbeatRef = (who, genesis) => {
393
- if (!SHA.test(genesis))
394
- C.fail('heartbeat requires a full room genesis');
395
- const ref = `${TAGS}${who}.heartbeat.${genesis.slice(0, 12)}`;
396
- if (!heartbeatName(ref))
397
- C.fail('malformed heartbeat actor');
398
- return ref;
399
- };
400
- const heartbeatBody = (text) => {
401
- if (Buffer.byteLength(text) > 16384)
402
- C.fail('heartbeat body exceeds 16 KiB');
403
- const decoded = P.decodeText(Buffer.from(text));
404
- if (decoded.invalid)
405
- C.fail(`heartbeat body is ${decoded.invalid}`);
406
- const lines = decoded.text.trimEnd().split('\n');
407
- const fields = new Map();
408
- for (const line of lines) {
409
- const m = /^([A-Z][A-Za-z]*): (.+)$/.exec(line) ?? C.fail('heartbeat body must contain only facts as trailers');
410
- if (fields.has(m[1]))
411
- C.fail(`duplicate heartbeat field ${m[1]}`);
412
- fields.set(m[1], m[2]);
413
- }
414
- const genesis = fields.get('Room') ?? C.fail('heartbeat must name its full room genesis');
415
- if (!SHA.test(genesis))
416
- C.fail('heartbeat must name its full room genesis');
417
- if (!/^(working|waiting|gone)$/.test(fields.get('State') ?? ''))
418
- C.fail('invalid heartbeat State');
419
- if (!/^(yes|no|unknown)$/.test(fields.get('Alive') ?? ''))
420
- C.fail('invalid heartbeat Alive');
421
- const next = Number(fields.get('Next'));
422
- if (!Number.isFinite(next) || next <= 0)
423
- C.fail('heartbeat Next must be positive seconds');
424
- return genesis;
425
- };
426
- const heartbeatObject = (H, ref, sha) => {
427
- const s = heartbeatName(ref) ?? C.fail('malformed heartbeat namespace');
428
- if (!SHA.test(sha) || git(H, ['cat-file', '-t', sha]).trim() !== 'tag')
429
- C.fail('heartbeat must be an annotated tag');
430
- if (Number(git(H, ['cat-file', '-s', sha])) > 32768)
431
- C.fail('heartbeat object exceeds 32 KiB');
432
- const decoded = P.decodeText(git(H, ['cat-file', 'tag', sha], { raw: true }));
433
- if (decoded.invalid)
434
- C.fail(`heartbeat tag is ${decoded.invalid}`);
435
- const [header, ...rest] = decoded.text.split('\n\n');
436
- const target = /^object ([0-9a-f]+)\ntype commit\ntag ([^\n]+)\ntagger ([^<\n]+) <[^\n]*> (\d+) [+-]\d{4}\n?$/.exec(header) ?? C.fail('malformed heartbeat tag headers');
437
- if (!SHA.test(target[1]) || target[2] !== ref.slice(TAGS.length) || target[3].trim() !== s.actor)
438
- C.fail('heartbeat tag name or actor disagrees');
439
- const text = rest.join('\n\n').split('-----BEGIN SSH SIGNATURE-----')[0];
440
- const genesis = heartbeatBody(text);
441
- if (!genesis.startsWith(s.genesis12))
442
- C.fail('heartbeat tag name and room genesis disagree');
443
- return { ...s, genesis, target: target[1], time: Number(target[4]), text };
444
- };
445
- const checkHeartbeat = (H, R, ref, sha) => {
446
- const s = heartbeatObject(H, ref, sha);
447
- if (s.genesis !== R.genesis)
448
- C.fail('heartbeat belongs to another room genesis');
449
- const b = branch(H, R, rev(H, R.ref) ?? C.fail('heartbeat room has no branch here'));
450
- // It is a replica's sync position, not a claim to author or execute the target message.
451
- if (!b.control.includes(s.target) && !b.authors.has(s.target) && !b.skipped.has(s.target))
452
- C.fail('heartbeat target is not on this room branch');
453
- const signer = b.entries.find(e => e.principal === s.actor) ?? C.fail(`unadmitted heartbeat principal ${s.actor}`);
454
- checkSignature(H, sha, s.actor, signer.key, 'tag');
455
- };
456
- export const verifyHeartbeat = (H, R, ref, sha) => {
457
- if (!isRemote(H))
458
- return null;
459
- if (H.remoteAlarms?.[ref])
460
- return H.remoteAlarms[ref];
461
- if (H.heartbeatErrors?.[ref])
462
- return H.heartbeatErrors[ref];
463
- try {
464
- checkHeartbeat(H, R, ref, sha);
465
- return null;
466
- }
467
- catch (e) {
468
- return reason(e);
469
- }
470
- };
471
- // Make an object without moving its ref. The caller performs local CAS before publishing with the same old OID.
472
- export const makeHeartbeat = (H, R, who, target, text, home) => {
473
- const ref = heartbeatRef(who, R.genesis);
474
- if (!SHA.test(target))
475
- C.fail('heartbeat target must be a full commit id');
476
- if (/^Room:/m.test(text))
477
- C.fail('heartbeat Room is transport metadata');
478
- const body = `Room: ${R.genesis}\n${text.trimEnd()}\n`;
479
- heartbeatBody(body);
480
- const raw = `object ${target}\ntype commit\ntag ${ref.slice(TAGS.length)}\ntagger ${who} <${who}@ringi> ${Math.floor(Date.now() / 1000)} +0000\n\n${body}`;
481
- const key = sign(H, who, home)[0]?.slice(2);
482
- const signature = key ? command('ssh-keygen', ['-Y', 'sign', '-n', 'git', '-f', key], { input: raw }) : '';
483
- const sha = git(H, ['mktag'], { input: raw + signature }).trim();
484
- if (isRemote(H))
485
- checkHeartbeat(H, R, ref, sha);
486
- return sha;
487
- };
488
- const SIGNAL_NAME = /^([a-z0-9]+(?:[_-][a-z0-9]+)*)\.([a-z][a-z0-9.-]*)\.([0-9a-f]{12})$/;
489
- const signalName = (ref) => {
490
- if (heartbeatName(ref))
491
- C.fail('heartbeat is not an immutable signal');
492
- const m = ref.startsWith(TAGS) && SIGNAL_NAME.exec(ref.slice(TAGS.length));
493
- if (!m)
494
- return C.fail('malformed signal tag namespace');
495
- return { actor: m[1], id: m[3], signal: m[2] };
496
- };
497
- // The signed header carries the full target, tag name and actor; there is no message body.
498
- const signalObject = (H, ref, sha) => {
499
- const s = signalName(ref);
500
- if (git(H, ['cat-file', '-t', sha]).trim() !== 'tag')
501
- C.fail('signal must be an annotated tag');
502
- const text = P.decodeText(git(H, ['cat-file', 'tag', sha], { raw: true }));
503
- if (text.invalid)
504
- C.fail(`signal tag is ${text.invalid}`);
505
- const [header, ...rest] = text.text.split('\n\n');
506
- const target = /^object ([0-9a-f]+)\ntype commit\ntag ([^\n]+)\ntagger ([^<\n]+) <[^\n]*> \d+ [+-]\d{4}\n?$/.exec(header) ?? C.fail('malformed signal tag headers');
507
- if (!SHA.test(target[1]) || !target[1].startsWith(s.id) || target[2] !== ref.slice('refs/tags/'.length) || target[3].trim() !== s.actor)
508
- C.fail('signal tag name, actor or target disagrees');
509
- const body = rest.join('\n\n').split('-----BEGIN SSH SIGNATURE-----')[0];
510
- if (body !== '')
511
- C.fail('signal tag message must be empty');
512
- return { ...s, message: target[1] };
513
- };
514
- // Derived signals belong to their message's author; read/taken marks may name another member's message.
515
- // The room is determined by the full target commit on its verified branch, never by a short name or a tag body.
516
- const checkSignal = (H, R, ref, sha) => {
517
- const s = signalObject(H, ref, sha);
518
- const b = branch(H, R, rev(H, R.ref) ?? C.fail('signal room has no branch here'));
519
- const signer = b.entries.find(e => e.principal === s.actor) ?? C.fail(`unadmitted signal principal ${s.actor}`);
520
- checkSignature(H, sha, s.actor, signer.key, 'tag');
521
- const mark = s.signal === 'read' || s.signal === 'taken';
522
- if (s.message === R.genesis)
523
- C.fail('signal points at no message of this room');
524
- const author = b.authors.get(s.message);
525
- if (!(mark ? author : author === s.actor))
526
- C.fail(`signal points at no accepted message ${mark ? 'of this room' : 'of its actor'}`);
527
- };
528
- export const verifySignal = (H, R, ref, sha) => {
529
- if (!isRemote(H))
530
- return null;
531
- const alarm = H.remoteAlarms?.[ref];
532
- if (alarm)
533
- return alarm;
534
- try {
535
- checkSignal(H, R, ref, sha);
536
- return null;
537
- }
538
- catch (e) {
539
- return reason(e);
540
- }
541
- };
542
- const refs = (H, prefix) => new Map(git(H, ['for-each-ref', '--format=%(refname) %(objectname)', prefix]).trim().split('\n').filter(Boolean).map(s => s.split(' ')));
543
- const pushAlarmsFile = (H) => path.join(H.dir, 'ringi', 'push-alarms.json');
544
- const alarmState = (text, label) => {
545
- const value = strictJson(text, label);
546
- if (!value || typeof value !== 'object' || Array.isArray(value) || !Object.values(value).every(v => typeof v === 'string'))
547
- C.fail(`${label}: expected an object of alarm strings`);
548
- return value;
549
- };
550
- const savePushAlarms = (H, alarms) => {
551
- const file = pushAlarmsFile(H);
552
- mkdir(path.dirname(file));
553
- C.writeJsonAtomic(file, alarms);
554
- };
555
- const pushAlarms = (H) => {
556
- const file = pushAlarmsFile(H);
557
- try {
558
- return alarmState(fs.readFileSync(file, 'utf8'), file);
559
- }
560
- catch (e) {
561
- if (C.errno(e) !== 'ENOENT')
562
- throw e;
563
- }
564
- const old = git(H, ['config', '--get', 'ringi.pushAlarms'], { ok: true });
565
- if (old.status !== 0 && old.status !== 1)
566
- C.fail(`cannot read legacy push alarm state: ${old.stderr.trim()}`);
567
- const alarms = alarmState(old.status === 1 ? '{}' : old.stdout, 'legacy ringi.pushAlarms');
568
- savePushAlarms(H, alarms);
569
- if (old.status === 0)
570
- git(H, ['config', '--unset-all', 'ringi.pushAlarms']);
571
- return alarms;
572
- };
573
- // The local accepted transport refs survive reader resets and retain evidence of remote rewrites/deletions.
574
- const promote = (H, ref, sha) => {
575
- const acc = rev(H, accepted(ref));
576
- const cur = rev(H, ref);
577
- if (acc && !ancestor(H, acc, sha))
578
- C.fail(`${suffix(ref)} was rewritten or rolled back from ${acc.slice(0, 12)} to ${sha.slice(0, 12)}`);
579
- if (cur && cur !== sha && !ancestor(H, cur, sha) && !ancestor(H, sha, cur))
580
- C.fail(`${suffix(ref)} diverges from the local accepted history`);
581
- if (!cur || (cur !== sha && ancestor(H, cur, sha)))
582
- git(H, ['update-ref', ref, sha, cur || zero(H)]);
583
- git(H, ['update-ref', accepted(ref), sha, acc || zero(H)]);
584
- };
585
- const syncUnlocked = (H) => {
586
- H.remoteAlarms = { ...pushAlarms(H) };
587
- H.heartbeatErrors = {};
588
- const url = origin(H);
589
- if (!url) {
590
- if (rev(H, 'refs/heads/main'))
591
- verifyHub(H);
592
- return H;
593
- }
594
- checkUrl(url);
595
- git(H, ['-c', 'fetch.fsckObjects=true', 'fetch', '-q', '--no-tags', '--prune', url,
596
- `+refs/heads/*:${TRACK}*`, `+${TAGS}*:${TAG_TRACK}*`]);
597
- const fetched = refs(H, TRACK);
598
- const main = fetched.get(TRACK + 'main') || C.fail('ALARM remote main was deleted or is missing; refusing authority from the hub');
599
- try {
600
- verifyMain(H, main);
601
- promote(H, 'refs/heads/main', main);
602
- }
603
- catch (e) {
604
- C.fail(`ALARM hub ${H.name}: ${reason(e)}; nothing is delivered`);
605
- }
606
- H.main = rev(H, 'refs/heads/main');
607
- // Every other branch is a room. Branches of ringi 0.1 (`<room>/control`, `<room>/log/<agent>`) are not read.
608
- const names = new Set([...fetched.keys(), ...refs(H, ACCEPT).keys()].map(r => r.slice(r.startsWith(TRACK) ? TRACK.length : ACCEPT.length)).filter(r => r !== 'main' && !r.includes('/')));
609
- const byGenesis = new Map(), byTarget = new Map();
610
- for (const name of names) {
611
- const ref = 'refs/heads/' + name;
612
- try {
613
- if (!P.ID_RE.test(name))
614
- C.fail('invalid room name');
615
- const sha = fetched.get(TRACK + name) ?? C.fail(`${name} was deleted on the remote`);
616
- const data = branchData(H, name, sha);
617
- if (byGenesis.has(data.genesis))
618
- C.fail('room genesis is already bound to another room');
619
- promote(H, ref, sha);
620
- // Local commits not yet published stay on top.
621
- const local = branchData(H, name, rev(H, ref), data.genesis);
622
- const R = { name, ref, head: local.control.at(-1), genesis: data.genesis };
623
- byGenesis.set(data.genesis, R);
624
- for (const target of [...local.control, ...local.authors.keys(), ...local.skipped.keys()])
625
- byTarget.set(target, R);
626
- }
627
- catch (e) {
628
- H.remoteAlarms[ref] = reason(e);
629
- }
630
- }
631
- const tags = refs(H, TAG_TRACK), pinned = refs(H, TAG_ACCEPT);
632
- // Include local beats to remove disappeared remote telemetry; unlike signals, nothing is pinned.
633
- for (const ref of refs(H, TAGS).keys())
634
- if (heartbeatName(ref) && !tags.has(TAG_TRACK + ref.slice(TAGS.length))) {
635
- if (pinned.has(TAG_ACCEPT + ref.slice(TAGS.length)))
636
- continue; // never waive previously accepted history
637
- git(H, ['update-ref', '-d', ref]);
638
- }
639
- for (const name of new Set([...tags.keys()].map(r => r.slice(TAG_TRACK.length)).concat([...pinned.keys()].map(r => r.slice(TAG_ACCEPT.length))))) {
640
- const beatRef = TAGS + name;
641
- if (heartbeatName(beatRef)) {
642
- if (pinned.has(TAG_ACCEPT + name)) {
643
- H.remoteAlarms[beatRef] = 'heartbeat namespace overlaps a previously accepted signal; operator review required';
644
- continue;
645
- }
646
- try {
647
- const sha = tags.get(TAG_TRACK + name) ?? C.fail('heartbeat is absent from the remote');
648
- const b = heartbeatObject(H, beatRef, sha);
649
- const R = byGenesis.get(b.genesis) ?? C.fail('heartbeat room is missing or quarantined');
650
- checkHeartbeat(H, R, beatRef, sha);
651
- git(H, ['update-ref', beatRef, sha]);
652
- }
653
- catch (e) {
654
- H.heartbeatErrors[beatRef] = reason(e);
655
- git(H, ['update-ref', '-d', beatRef]);
656
- note(`heartbeat ${name}: ${reason(e)}; withheld`);
657
- }
658
- continue;
659
- }
660
- // Other tags, including the old room-scoped namespace, are not signals.
661
- if (!SIGNAL_NAME.test(name))
662
- continue;
663
- const ref = TAGS + name;
664
- try {
665
- const sha = tags.get(TAG_TRACK + name) ?? C.fail('signal was deleted on the remote after acceptance');
666
- const s = signalObject(H, ref, sha);
667
- const R = byTarget.get(s.message) ?? C.fail('signal room is missing or quarantined');
668
- checkSignal(H, R, ref, sha);
669
- const acc = pinned.get(TAG_ACCEPT + name), cur = object(H, ref);
670
- if (acc && acc !== sha)
671
- C.fail('signal was replaced after transport acceptance');
672
- if (cur && cur !== sha)
673
- C.fail('signal diverges from the local tag object');
674
- git(H, ['update-ref', ref, sha, cur ?? zero(H)]);
675
- git(H, ['update-ref', TAG_ACCEPT + name, sha, acc ?? zero(H)]);
676
- }
677
- catch (e) {
678
- H.remoteAlarms[ref] = reason(e);
679
- }
680
- }
681
- for (const [ref, why] of Object.entries(H.remoteAlarms))
682
- note(`ALARM ${suffix(ref)}: ${why}; quarantined and withheld`);
683
- return H;
684
- };
685
- export const sync = (H) => !isRemote(H) ? H : locked(H, () => syncUnlocked(H));
686
- export const hubAdd = ({ home, name, url, root, genesis }) => {
687
- id(name);
688
- checkUrl(url);
689
- if (!root)
690
- C.fail('hub add requires --root <public-key|file> from the operator; a key advertised by the hub is not a trust anchor');
691
- const key = keyInput(root ?? C.fail('hub add requires --root <public-key|file>'));
692
- if (genesis && !SHA.test(genesis))
693
- C.fail('--genesis must be a full commit id');
694
- // A hub is a descriptor (hub.json) beside its store; for a git store the repository is store/.git.
695
- const hubDir = path.join(home, 'hub', name);
696
- const dir = path.join(hubDir, 'store', '.git');
697
- if (fs.existsSync(hubDir))
698
- C.fail(`hub ${name} exists`);
699
- mkdir(path.dirname(dir));
700
- const stage = path.join(home, 'hub', `.${name}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.git`);
701
- try {
702
- command('git', ['init', '--bare', '-q', '-b', 'main', stage]);
703
- const descriptor = { schema: 'ringi-hub/2', name, trust: 'signed', root: key.line, genesis,
704
- store: { type: 'git', path: 'store', remote: url } };
705
- const H = { name, dir: stage, d: descriptor, bootstrap: !genesis };
706
- sync(H);
707
- fs.renameSync(stage, dir);
708
- fs.writeFileSync(path.join(hubDir, 'hub.json'), JSON.stringify(descriptor, null, 2) + '\n', { mode: 0o600 });
709
- return dir;
710
- }
711
- finally {
712
- if (fs.existsSync(stage))
713
- fs.rmSync(stage, { recursive: true, force: true });
714
- // A failed add leaves no half-made hub behind (the directory did not exist before).
715
- if (!fs.existsSync(dir))
716
- fs.rmSync(hubDir, { recursive: true, force: true });
717
- }
718
- };
719
- export const join = (H, R, who, home) => {
720
- const room = verifyRoom(H, R);
721
- const key = locked(H, () => localKey(H, who, home, true));
722
- const admitted = !!room.entries.find(e => e.principal === who && e.key.line === key.line);
723
- return { publicKey: key.line, fingerprint: key.fp, admitted };
724
- };
725
- // A tree from `base` with the control files as `control` has them, then `patches` on top.
726
- const treeWith = (H, base, control, patches) => {
727
- const entries = (sha) => new Map(git(H, ['ls-tree', sha]).trim().split('\n').filter(Boolean).map(line => {
728
- const m = /^(\d+) (\w+) ([0-9a-f]+)\t(.+)$/.exec(line) ?? C.fail('malformed room tree');
729
- return [m[4], line];
730
- }));
731
- const files = entries(base), kept = entries(control);
732
- for (const name of CONTROL_FILES) {
733
- files.delete(name);
734
- if (kept.has(name))
735
- files.set(name, kept.get(name));
736
- }
737
- for (const [name, content] of Object.entries(patches))
738
- files.set(name, `100644 blob ${git(H, ['hash-object', '-w', '--stdin'], { input: content }).trim()}\t${name}`);
739
- return git(H, ['mktree'], { input: [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, line]) => line).join('\n') + '\n' }).trim();
740
- };
741
- // The root's control record admitting a member, on top of the room's branch: the key and the member go in together.
742
- export const roomAdmit = (H, R, who, publicKey, home) => {
743
- id(who);
744
- if (!isRemote(H))
745
- C.fail('room admit is for signed hubs; local membership is added by join');
746
- if (who === ROOT)
747
- C.fail('the root is already pinned; root replacement requires a separate trust transition');
748
- if (R.archived)
749
- C.fail('cannot admit into an archived room');
750
- const key = keyInput(publicKey);
751
- for (let attempt = 0; attempt < 8; attempt++) {
752
- const tip = rev(H, R.ref) ?? C.fail('room branch is missing');
753
- const room = branch(H, R, tip), head = room.control.at(-1);
754
- if (room.entries.some(e => e.principal === who || e.key.fp === key.fp))
755
- C.fail('principal or key is already admitted; key rotation is not implemented');
756
- const members = { ...room.members, [who]: { role: 'member', since: new Date().toISOString() } };
757
- const roster = [...room.entries.map(e => signerLine(e.principal, e.key)), signerLine(who, key)].join('');
758
- const tree = treeWith(H, tip, head, { 'members.json': JSON.stringify(members, null, 2) + '\n', allowed_signers: roster });
759
- const sha = git(H, ['commit-tree', ...sign(H, ROOT, home), tree, '-p', tip], {
760
- input: P.compose(`admit ${who}`, [['To', '*'], ['Kind', 'room'], ['Key', key.fp]]), env: ident(ROOT),
761
- }).trim();
762
- if (git(H, ['update-ref', R.ref, sha, tip], { ok: true }).status !== 0)
763
- continue;
764
- if (!push(H, R.ref, { behind: true }))
765
- continue;
766
- R.head = sha;
767
- R.members = members;
768
- return sha;
769
- }
770
- return C.fail('room kept moving while admitting; retry');
771
- };
772
- const validateRef = (H, ref, sha) => {
773
- if (ref.startsWith(TAGS)) {
774
- const s = signalObject(H, ref, sha);
775
- for (const [branchRef, head] of refs(H, 'refs/heads/')) {
776
- const name = branchRef.slice('refs/heads/'.length);
777
- if (name === 'main' || name.includes('/') || !ancestor(H, s.message, head))
778
- continue;
779
- const b = branchData(H, name, head);
780
- if (b.control.includes(s.message) || b.authors.has(s.message) || b.skipped.has(s.message)) {
781
- checkSignal(H, { name, ref: branchRef, head: b.control.at(-1), genesis: b.genesis }, ref, sha);
782
- return;
783
- }
784
- }
785
- return C.fail('signal points at no verified room');
786
- }
787
- if (ref === 'refs/heads/main') {
788
- verifyMain(H, sha);
789
- return;
790
- }
791
- const room = /^refs\/heads\/([^/]+)$/.exec(ref) ?? C.fail('only explicit main, room or signal refs may be pushed');
792
- if (!P.ID_RE.test(room[1]))
793
- C.fail('only explicit main, room or signal refs may be pushed');
794
- const why = branchData(H, room[1], sha).skipped.get(sha);
795
- if (why)
796
- C.fail(`${sha.slice(0, 12)} is no valid commit of room ${room[1]}: ${why}`);
797
- };
798
- // Publish one ref. With `behind`, a room's branch the remote has moved on is not an alarm: push returns false and the
799
- // caller makes its commit again on the new tip.
800
- export const push = (H, ref, { behind = false } = {}) => {
801
- if (heartbeatName(ref))
802
- C.fail('heartbeat publication requires its actor and expected object id (pushHeartbeat)');
803
- if (!isRemote(H))
804
- return true;
805
- return locked(H, () => {
806
- const alarms = pushAlarms(H);
807
- // Read the current head after taking the transport lock: an earlier append may have waited behind a newer one.
808
- const tag = ref.startsWith(TAGS);
809
- const pin = tag ? TAG_ACCEPT + ref.slice(TAGS.length) : accepted(ref);
810
- const track = tag ? TAG_TRACK + ref.slice(TAGS.length) : tracking(ref);
811
- const sha = (tag ? object(H, ref) : rev(H, ref)) || C.fail(`no ${ref} to publish`);
812
- validateRef(H, ref, sha);
813
- const url = origin(H);
814
- if (!url)
815
- return true; // signed source may be provisioned before a remote is attached
816
- checkUrl(url);
817
- const acc = tag ? object(H, pin) : rev(H, pin);
818
- if (acc && (tag ? acc !== sha : !ancestor(H, acc, sha)))
819
- C.fail('ref to publish does not extend its accepted transport history');
820
- const r = git(H, ['push', '-q', '--porcelain', url, `${sha}:${ref}`], { ok: true });
821
- if (r.status !== 0) {
822
- let why = '';
823
- if (behind && !tag && ref !== 'refs/heads/main') {
824
- try {
825
- git(H, ['-c', 'fetch.fsckObjects=true', 'fetch', '-q', '--no-tags', url, `+${ref}:${track}`]);
826
- const incoming = rev(H, track) ?? C.fail('remote room branch is missing');
827
- const data = branchData(H, suffix(ref), sha);
828
- branchData(H, suffix(ref), incoming, data.genesis);
829
- if (acc && !ancestor(H, acc, incoming))
830
- C.fail('remote room was rewritten or rolled back; retry refused');
831
- if (ancestor(H, sha, incoming)) {
832
- // The remote received the message but its acknowledgement was lost.
833
- promote(H, ref, incoming);
834
- delete alarms[ref];
835
- savePushAlarms(H, alarms);
836
- return true;
837
- }
838
- const parents = git(H, ['show', '-s', '--format=%P', sha]).trim().split(' ');
839
- if (/\[rejected\].*\((?:fetch first|non-fast-forward)\)/.test(String(r.stdout)) && acc && parents.length === 1 && parents[0] === acc) {
840
- // Only one unpublished message may be retried. Its caller has not returned an id yet.
841
- git(H, ['update-ref', `refs/ringi/unpublished/${sha}`, sha]);
842
- git(H, ['update-ref', ref, incoming, sha]);
843
- git(H, ['update-ref', pin, incoming, acc]);
844
- delete alarms[ref];
845
- savePushAlarms(H, alarms);
846
- if (H.remoteAlarms)
847
- delete H.remoteAlarms[ref];
848
- return false;
849
- }
850
- }
851
- catch (e) {
852
- why = `; ${reason(e)}`;
853
- }
854
- }
855
- alarms[ref] = `push rejected for ${suffix(ref)}; local commit ${sha.slice(0, 12)} retained; resolve the remote conflict before continuing${why}`;
856
- savePushAlarms(H, alarms);
857
- C.fail(`ALARM ${alarms[ref]}`);
858
- }
859
- delete alarms[ref];
860
- savePushAlarms(H, alarms);
861
- git(H, ['update-ref', pin, sha, acc || zero(H)]);
862
- git(H, ['update-ref', track, sha]);
863
- return true;
864
- });
865
- };
866
- // Publish only the actor's exact heartbeat with an explicit old OID. A racing beat is ordinary contention, never a
867
- // signal rewrite alarm. Neither success nor failure changes immutable acceptance pins or budget/history state.
868
- export const pushHeartbeat = (H, R, who, sha, expected) => {
869
- const ref = heartbeatRef(who, R.genesis);
870
- if (!SHA.test(sha) || (expected !== null && !SHA.test(expected)))
871
- C.fail('heartbeat publication requires full object ids');
872
- if (!isRemote(H))
873
- return true;
874
- return locked(H, () => {
875
- if (object(H, ref) !== sha)
876
- return false; // a newer local beat already won
877
- if (object(H, TAG_ACCEPT + ref.slice(TAGS.length)))
878
- C.fail('heartbeat namespace overlaps previously accepted signal history');
879
- checkHeartbeat(H, R, ref, sha);
880
- const url = origin(H);
881
- if (!url)
882
- return true;
883
- checkUrl(url);
884
- const r = git(H, ['push', '-q', '--porcelain', `--force-with-lease=${ref}:${expected ?? ''}`, url, `${sha}:${ref}`], { ok: true });
885
- if (r.status !== 0) {
886
- // A lost acknowledgement is safe to reconcile: equality proves no second beat is needed.
887
- const observed = git(H, ['ls-remote', '--refs', url, ref], { ok: true });
888
- const got = observed.status === 0 ? observed.stdout.trim().split(/\s+/) : [];
889
- if (got[0] !== sha || got[1] !== ref) {
890
- if (/\[rejected\].*\(stale info\)/.test(r.stdout))
891
- return false;
892
- C.fail(`heartbeat push failed for ${ref.slice(TAGS.length)}: ${String(r.stderr || r.stdout || 'transport error').trim().split('\n').at(-1)}`);
893
- }
894
- }
895
- git(H, ['update-ref', TAG_TRACK + ref.slice(TAGS.length), sha]);
896
- if (H.heartbeatErrors)
897
- delete H.heartbeatErrors[ref];
898
- return true;
899
- });
900
- };
901
- // Only the operator command calls this after reviewing a room's transport alarm: the room's branch as the remote has
902
- // it becomes the accepted baseline, and the displaced local branch is kept as evidence. Nothing is waived: the branch
903
- // must verify, and its invalid commits stay skipped.
904
- export const forget = (H, R) => {
905
- if (!isRemote(H))
906
- return;
907
- return locked(H, () => {
908
- const alarms = pushAlarms(H);
909
- if (!alarms[R.ref] && !H.remoteAlarms?.[R.ref])
910
- return;
911
- const sha = rev(H, tracking(R.ref));
912
- if (sha)
913
- branchData(H, R.name, sha, R.genesis);
914
- const old = rev(H, R.ref);
915
- if (old)
916
- git(H, ['update-ref', `refs/ringi/quarantine/${R.genesis}/${old}`, old]);
917
- git(H, ['update-ref', '-d', accepted(R.ref)]);
918
- if (sha)
919
- git(H, ['update-ref', R.ref, sha, old || zero(H)]);
920
- else if (old)
921
- git(H, ['update-ref', '-d', R.ref, old]);
922
- delete alarms[R.ref];
923
- savePushAlarms(H, alarms);
924
- if (H.remoteAlarms)
925
- delete H.remoteAlarms[R.ref];
926
- });
927
- };
928
- //# sourceMappingURL=remote.js.map