agents-city 0.3.0-beta.21

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 (440) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.es.md +2021 -0
  5. package/README.md +1999 -0
  6. package/benchmarks/committee/README.md +39 -0
  7. package/benchmarks/committee/metrics.py +51 -0
  8. package/benchmarks/committee/run.py +64 -0
  9. package/benchmarks/committee/traces/01-single.json +8 -0
  10. package/benchmarks/committee/traces/02-mesh.json +16 -0
  11. package/benchmarks/committee/traces/03-chair.json +15 -0
  12. package/benchmarks/latency/README.md +35 -0
  13. package/benchmarks/latency/fake-claude-cli.mjs +119 -0
  14. package/benchmarks/latency/fake-native-server.mjs +340 -0
  15. package/benchmarks/latency/fake-tui.py +91 -0
  16. package/benchmarks/latency/live.py +380 -0
  17. package/benchmarks/stress/README.md +20 -0
  18. package/benchmarks/stress/run.py +487 -0
  19. package/bin/agents +4 -0
  20. package/bin/agents-city.js +116 -0
  21. package/bin/benchmark +17 -0
  22. package/bin/bus +6 -0
  23. package/bin/cities +3 -0
  24. package/bin/city +86 -0
  25. package/bin/committee +6 -0
  26. package/bin/demo +200 -0
  27. package/bin/exit +5 -0
  28. package/bin/hall +73 -0
  29. package/bin/hall.html +426 -0
  30. package/bin/logs +3 -0
  31. package/bin/report.py +14 -0
  32. package/bin/reset +3 -0
  33. package/bin/road +3 -0
  34. package/bin/seat +10 -0
  35. package/bin/serve.py +1428 -0
  36. package/bin/setup.py +753 -0
  37. package/bin/skills +3 -0
  38. package/bin/test +131 -0
  39. package/bin/test-adapter.py +183 -0
  40. package/bin/test-admision.py +99 -0
  41. package/bin/test-avatar.py +58 -0
  42. package/bin/test-benchmark.py +40 -0
  43. package/bin/test-broker.py +334 -0
  44. package/bin/test-cage.py +206 -0
  45. package/bin/test-card.py +402 -0
  46. package/bin/test-channel.py +379 -0
  47. package/bin/test-cities.py +453 -0
  48. package/bin/test-claude-runtime.py +252 -0
  49. package/bin/test-committee.py +340 -0
  50. package/bin/test-contracts.py +544 -0
  51. package/bin/test-crecimiento.py +164 -0
  52. package/bin/test-demo.py +308 -0
  53. package/bin/test-doctor.py +106 -0
  54. package/bin/test-domains.py +151 -0
  55. package/bin/test-evidencia.py +50 -0
  56. package/bin/test-exit.py +128 -0
  57. package/bin/test-hall-protocol.py +93 -0
  58. package/bin/test-launch.py +169 -0
  59. package/bin/test-live-feed.py +508 -0
  60. package/bin/test-pairing.py +99 -0
  61. package/bin/test-parcels.py +283 -0
  62. package/bin/test-runtime-failures.py +252 -0
  63. package/bin/test-runtime-ui.py +488 -0
  64. package/bin/test-runtime.py +268 -0
  65. package/bin/test-rutas.py +80 -0
  66. package/bin/test-seat.py +1386 -0
  67. package/bin/test-security.py +150 -0
  68. package/bin/test-serve.py +1057 -0
  69. package/bin/test-stress.py +64 -0
  70. package/bin/test-widgets.py +143 -0
  71. package/bin/test-workspace.py +188 -0
  72. package/bin/testlib.py +95 -0
  73. package/bin/tokens.py +14 -0
  74. package/bus/scripts/deploy.sh +60 -0
  75. package/bus/scripts/mint-token.sh +68 -0
  76. package/bus/scripts/setup-dev.sh +79 -0
  77. package/bus/scripts/test-channel.ts +8 -0
  78. package/bus/scripts/test-hub.ts +266 -0
  79. package/bus/scripts/test-local.sh +40 -0
  80. package/bus/scripts/test-queue.ts +132 -0
  81. package/bus/worker/package-lock.json +1971 -0
  82. package/bus/worker/package.json +17 -0
  83. package/bus/worker/src/index.ts +485 -0
  84. package/bus/worker/tsconfig.json +13 -0
  85. package/bus/worker/wrangler.toml +32 -0
  86. package/city/oven/README.md +59 -0
  87. package/city/oven/collect.py +43 -0
  88. package/city/oven/oven.html +4 -0
  89. package/city/scripts/history.py +120 -0
  90. package/city/scripts/seed.py +442 -0
  91. package/city/web/assets/modelos/License.txt +28 -0
  92. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  93. package/city/web/assets/modelos/building-a.glb +0 -0
  94. package/city/web/assets/modelos/building-b.glb +0 -0
  95. package/city/web/assets/modelos/building-c.glb +0 -0
  96. package/city/web/assets/modelos/building-d.glb +0 -0
  97. package/city/web/assets/modelos/building-e.glb +0 -0
  98. package/city/web/assets/modelos/building-f.glb +0 -0
  99. package/city/web/assets/modelos/building-g.glb +0 -0
  100. package/city/web/assets/modelos/building-h.glb +0 -0
  101. package/city/web/assets/sprites/building-a.png +0 -0
  102. package/city/web/assets/sprites/building-b.png +0 -0
  103. package/city/web/assets/sprites/building-c.png +0 -0
  104. package/city/web/assets/sprites/building-d.png +0 -0
  105. package/city/web/assets/sprites/building-e.png +0 -0
  106. package/city/web/assets/sprites/building-f.png +0 -0
  107. package/city/web/assets/sprites/building-g.png +0 -0
  108. package/city/web/assets/sprites/building-h.png +0 -0
  109. package/city/web/assets/sprites/building-i.png +0 -0
  110. package/city/web/assets/sprites/building-j.png +0 -0
  111. package/city/web/assets/sprites/building-k.png +0 -0
  112. package/city/web/assets/sprites/building-l.png +0 -0
  113. package/city/web/assets/sprites/building-m.png +0 -0
  114. package/city/web/assets/sprites/building-n.png +0 -0
  115. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  116. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  117. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  118. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  119. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  120. package/city/web/assets/sprites/catalogo.json +47 -0
  121. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  122. package/city/web/assets/sprites/detail-awning.png +0 -0
  123. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  124. package/city/web/assets/sprites/detail-overhang.png +0 -0
  125. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  126. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  127. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  128. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  129. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  130. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  131. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  132. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  133. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  143. package/city/web/assets/sprites/medidas.json +248 -0
  144. package/city/web/dist/city.js +939 -0
  145. package/city/web/dist/index.html +294 -0
  146. package/city/web/dist/modelos/License.txt +28 -0
  147. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  148. package/city/web/dist/modelos/building-a.glb +0 -0
  149. package/city/web/dist/modelos/building-b.glb +0 -0
  150. package/city/web/dist/modelos/building-c.glb +0 -0
  151. package/city/web/dist/modelos/building-d.glb +0 -0
  152. package/city/web/dist/modelos/building-e.glb +0 -0
  153. package/city/web/dist/modelos/building-f.glb +0 -0
  154. package/city/web/dist/modelos/building-g.glb +0 -0
  155. package/city/web/dist/modelos/building-h.glb +0 -0
  156. package/city/web/dist/sprites/building-a.png +0 -0
  157. package/city/web/dist/sprites/building-b.png +0 -0
  158. package/city/web/dist/sprites/building-c.png +0 -0
  159. package/city/web/dist/sprites/building-d.png +0 -0
  160. package/city/web/dist/sprites/building-e.png +0 -0
  161. package/city/web/dist/sprites/building-f.png +0 -0
  162. package/city/web/dist/sprites/building-g.png +0 -0
  163. package/city/web/dist/sprites/building-h.png +0 -0
  164. package/city/web/dist/sprites/building-i.png +0 -0
  165. package/city/web/dist/sprites/building-j.png +0 -0
  166. package/city/web/dist/sprites/building-k.png +0 -0
  167. package/city/web/dist/sprites/building-l.png +0 -0
  168. package/city/web/dist/sprites/building-m.png +0 -0
  169. package/city/web/dist/sprites/building-n.png +0 -0
  170. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  171. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  172. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  173. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  174. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  175. package/city/web/dist/sprites/catalogo.json +47 -0
  176. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  177. package/city/web/dist/sprites/detail-awning.png +0 -0
  178. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  179. package/city/web/dist/sprites/detail-overhang.png +0 -0
  180. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  181. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  182. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  183. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  184. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  185. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  186. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  187. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  188. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  198. package/city/web/dist/sprites/medidas.json +248 -0
  199. package/city/web/dist-hall/hall.js +264 -0
  200. package/city/web/index.html +294 -0
  201. package/city/web/package.json +18 -0
  202. package/city/web/sella.py +21 -0
  203. package/city/web/src/activity-actors.ts +47 -0
  204. package/city/web/src/activity.ts +172 -0
  205. package/city/web/src/ayuntamiento.ts +580 -0
  206. package/city/web/src/draw.ts +640 -0
  207. package/city/web/src/game-speech.ts +79 -0
  208. package/city/web/src/hall.ts +1867 -0
  209. package/city/web/src/main.ts +2225 -0
  210. package/city/web/src/oven.ts +124 -0
  211. package/city/web/src/people.ts +244 -0
  212. package/city/web/src/presencia.ts +193 -0
  213. package/city/web/src/puertas.ts +180 -0
  214. package/city/web/src/repo-roles.ts +59 -0
  215. package/city/worker/package.json +16 -0
  216. package/city/worker/schema.sql +117 -0
  217. package/city/worker/src/index.ts +702 -0
  218. package/city/worker/src/square.ts +218 -0
  219. package/city/worker/wrangler.toml +58 -0
  220. package/demo/ada.md +50 -0
  221. package/demo/bruno.md +62 -0
  222. package/demo/camila.md +56 -0
  223. package/demo/city/ada.md +40 -0
  224. package/demo/city/city.yml +8 -0
  225. package/demo/city/parcels.yml +15 -0
  226. package/demo/city/roads.json +4 -0
  227. package/demo/city/units.yml +10 -0
  228. package/demo/clinica/city.yml +8 -0
  229. package/demo/clinica/parcels.yml +15 -0
  230. package/demo/clinica/roads.json +4 -0
  231. package/demo/clinica/units.yml +9 -0
  232. package/demo/clinica/vera.md +46 -0
  233. package/demo/dante.md +54 -0
  234. package/demo/despacho/city.yml +8 -0
  235. package/demo/despacho/marta.md +46 -0
  236. package/demo/despacho/parcels.yml +15 -0
  237. package/demo/despacho/roads.json +4 -0
  238. package/demo/despacho/units.yml +9 -0
  239. package/demo/elsa.md +50 -0
  240. package/demo/farid.md +52 -0
  241. package/demo/greta.md +50 -0
  242. package/demo/hugo.md +49 -0
  243. package/demo/iris.md +49 -0
  244. package/demo/jonas.md +51 -0
  245. package/demo/kira.md +51 -0
  246. package/demo/luca.md +49 -0
  247. package/demo/parcels.yml +92 -0
  248. package/demo/seed.py +20 -0
  249. package/demo/show.py +161 -0
  250. package/demo/stories.py +824 -0
  251. package/demo/units.yml +33 -0
  252. package/docs/agents-first.md +99 -0
  253. package/docs/glossary.md +55 -0
  254. package/docs/map-live-layers.md +73 -0
  255. package/docs/security.md +231 -0
  256. package/docs/self-host.md +151 -0
  257. package/docs/testing.md +205 -0
  258. package/package.json +73 -0
  259. package/plugin/.claude-plugin/plugin.json +62 -0
  260. package/plugin/.mcp.json +15 -0
  261. package/plugin/channel/activity-cli.ts +16 -0
  262. package/plugin/channel/adapter-prompts.ts +113 -0
  263. package/plugin/channel/adapter.js +4292 -0
  264. package/plugin/channel/adapter.ts +133 -0
  265. package/plugin/channel/bus.js +20188 -0
  266. package/plugin/channel/bus.ts +131 -0
  267. package/plugin/channel/city-config.ts +125 -0
  268. package/plugin/channel/claude-channel.ts +58 -0
  269. package/plugin/channel/cli-args.ts +75 -0
  270. package/plugin/channel/client.js +4200 -0
  271. package/plugin/channel/client.ts +38 -0
  272. package/plugin/channel/committee/activity.ts +256 -0
  273. package/plugin/channel/committee/collection.ts +154 -0
  274. package/plugin/channel/committee/decision.ts +175 -0
  275. package/plugin/channel/committee/floor.ts +135 -0
  276. package/plugin/channel/committee/guards.ts +30 -0
  277. package/plugin/channel/committee/history.ts +38 -0
  278. package/plugin/channel/committee/render.ts +102 -0
  279. package/plugin/channel/committee/service.ts +72 -0
  280. package/plugin/channel/committee/storage.ts +92 -0
  281. package/plugin/channel/committee/types.ts +163 -0
  282. package/plugin/channel/committee/view.ts +63 -0
  283. package/plugin/channel/committee-cli.ts +109 -0
  284. package/plugin/channel/delivery-metrics.ts +61 -0
  285. package/plugin/channel/delivery-queue.ts +118 -0
  286. package/plugin/channel/hub/activity-controller.ts +72 -0
  287. package/plugin/channel/hub/activity-feed.ts +169 -0
  288. package/plugin/channel/hub/committee-controller.ts +60 -0
  289. package/plugin/channel/hub/connections.ts +48 -0
  290. package/plugin/channel/hub/diagnostics.ts +67 -0
  291. package/plugin/channel/hub/envelope-validity.ts +109 -0
  292. package/plugin/channel/hub/envelopes.ts +66 -0
  293. package/plugin/channel/hub/lifecycle.ts +91 -0
  294. package/plugin/channel/hub/local-roads.ts +89 -0
  295. package/plugin/channel/hub/remote-roads.ts +165 -0
  296. package/plugin/channel/hub/road-controller.ts +99 -0
  297. package/plugin/channel/hub-client.ts +149 -0
  298. package/plugin/channel/local-hub.js +5995 -0
  299. package/plugin/channel/local-hub.ts +349 -0
  300. package/plugin/channel/map-reporter.ts +48 -0
  301. package/plugin/channel/package-lock.json +1731 -0
  302. package/plugin/channel/package.json +19 -0
  303. package/plugin/channel/protocol.ts +98 -0
  304. package/plugin/channel/road-cli.ts +18 -0
  305. package/plugin/channel/run.sh +15 -0
  306. package/plugin/channel/runtime/claude.ts +458 -0
  307. package/plugin/channel/runtime/codex-config.ts +77 -0
  308. package/plugin/channel/runtime/codex.ts +669 -0
  309. package/plugin/channel/runtime/command.ts +62 -0
  310. package/plugin/channel/runtime/factory.ts +15 -0
  311. package/plugin/channel/runtime/json-rpc.ts +154 -0
  312. package/plugin/channel/runtime/kimi.ts +277 -0
  313. package/plugin/channel/runtime/opencode.ts +248 -0
  314. package/plugin/channel/runtime/process.ts +84 -0
  315. package/plugin/channel/runtime/types.ts +45 -0
  316. package/plugin/channel/runtime-files.ts +73 -0
  317. package/plugin/channel/runtime-gateway.js +6396 -0
  318. package/plugin/channel/runtime-gateway.ts +358 -0
  319. package/plugin/channel/runtime-metrics.ts +52 -0
  320. package/plugin/channel/runtime-subscription.ts +180 -0
  321. package/plugin/channel/terminal-delivery.ts +131 -0
  322. package/plugin/channel/untrusted.ts +60 -0
  323. package/plugin/commands/committee.md +31 -0
  324. package/plugin/commands/exit.md +32 -0
  325. package/plugin/commands/goals.md +20 -0
  326. package/plugin/commands/join.md +18 -0
  327. package/plugin/commands/notice.md +20 -0
  328. package/plugin/commands/propose.md +19 -0
  329. package/plugin/commands/round.md +17 -0
  330. package/plugin/commands/session.md +22 -0
  331. package/plugin/commands/settings.md +25 -0
  332. package/plugin/commands/setup.md +21 -0
  333. package/plugin/commands/team.md +23 -0
  334. package/plugin/domains/custom.md +27 -0
  335. package/plugin/domains/finance.md +31 -0
  336. package/plugin/domains/healthcare.md +34 -0
  337. package/plugin/domains/legal.md +31 -0
  338. package/plugin/domains/marketing.md +33 -0
  339. package/plugin/domains/operations.md +31 -0
  340. package/plugin/domains/research.md +32 -0
  341. package/plugin/domains/sales.md +30 -0
  342. package/plugin/domains/software.md +37 -0
  343. package/plugin/hooks/activity.sh +6 -0
  344. package/plugin/hooks/digging.sh +68 -0
  345. package/plugin/hooks/growth.sh +42 -0
  346. package/plugin/hooks/hooks.json +106 -0
  347. package/plugin/hooks/notice-on-pr.sh +35 -0
  348. package/plugin/hooks/notice-on-stop.sh +80 -0
  349. package/plugin/hooks/notice-pending.sh +58 -0
  350. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  351. package/plugin/hooks/tokens.sh +47 -0
  352. package/plugin/roles/examples/account-executive.md +25 -0
  353. package/plugin/roles/examples/ai-manager.md +34 -0
  354. package/plugin/roles/examples/associate.md +24 -0
  355. package/plugin/roles/examples/brand-lead.md +20 -0
  356. package/plugin/roles/examples/cfo.md +20 -0
  357. package/plugin/roles/examples/city-lead.md +25 -0
  358. package/plugin/roles/examples/clinical-director.md +26 -0
  359. package/plugin/roles/examples/clinical-ops.md +25 -0
  360. package/plugin/roles/examples/clinician.md +25 -0
  361. package/plugin/roles/examples/compliance.md +27 -0
  362. package/plugin/roles/examples/content.md +24 -0
  363. package/plugin/roles/examples/controller.md +27 -0
  364. package/plugin/roles/examples/cpto.md +41 -0
  365. package/plugin/roles/examples/customer-success.md +25 -0
  366. package/plugin/roles/examples/data-engineer.md +37 -0
  367. package/plugin/roles/examples/data.md +60 -0
  368. package/plugin/roles/examples/dev.md +37 -0
  369. package/plugin/roles/examples/devops.md +39 -0
  370. package/plugin/roles/examples/enablement.md +24 -0
  371. package/plugin/roles/examples/ethics.md +24 -0
  372. package/plugin/roles/examples/fin-analytics.md +27 -0
  373. package/plugin/roles/examples/health-compliance.md +25 -0
  374. package/plugin/roles/examples/health-data.md +26 -0
  375. package/plugin/roles/examples/knowledge.md +25 -0
  376. package/plugin/roles/examples/lifecycle.md +26 -0
  377. package/plugin/roles/examples/llm-engineer.md +34 -0
  378. package/plugin/roles/examples/managing-partner.md +20 -0
  379. package/plugin/roles/examples/methods.md +23 -0
  380. package/plugin/roles/examples/operations-lead.md +23 -0
  381. package/plugin/roles/examples/ops.md +24 -0
  382. package/plugin/roles/examples/patient-safety.md +26 -0
  383. package/plugin/roles/examples/performance.md +27 -0
  384. package/plugin/roles/examples/po.md +36 -0
  385. package/plugin/roles/examples/process-owner.md +23 -0
  386. package/plugin/roles/examples/product-design.md +39 -0
  387. package/plugin/roles/examples/program-manager.md +24 -0
  388. package/plugin/roles/examples/quality.md +25 -0
  389. package/plugin/roles/examples/research-director.md +24 -0
  390. package/plugin/roles/examples/research-ops.md +25 -0
  391. package/plugin/roles/examples/researcher.md +25 -0
  392. package/plugin/roles/examples/revenue-lead.md +25 -0
  393. package/plugin/roles/examples/revops.md +26 -0
  394. package/plugin/roles/examples/seo.md +27 -0
  395. package/plugin/roles/examples/specialist.md +24 -0
  396. package/plugin/scripts/admision.py +165 -0
  397. package/plugin/scripts/apaga.py +232 -0
  398. package/plugin/scripts/avatar.py +211 -0
  399. package/plugin/scripts/broker.py +531 -0
  400. package/plugin/scripts/cage.py +252 -0
  401. package/plugin/scripts/capabilities.py +197 -0
  402. package/plugin/scripts/card.py +312 -0
  403. package/plugin/scripts/cities.py +520 -0
  404. package/plugin/scripts/city-env.sh +90 -0
  405. package/plugin/scripts/city-runtime.sh +87 -0
  406. package/plugin/scripts/city-session.sh +526 -0
  407. package/plugin/scripts/city_env.py +67 -0
  408. package/plugin/scripts/crecimiento.py +140 -0
  409. package/plugin/scripts/deliberations.py +48 -0
  410. package/plugin/scripts/doctor.py +172 -0
  411. package/plugin/scripts/domains.py +201 -0
  412. package/plugin/scripts/evidencia.py +64 -0
  413. package/plugin/scripts/find-repos.sh +111 -0
  414. package/plugin/scripts/gh.py +159 -0
  415. package/plugin/scripts/hall_protocol.py +147 -0
  416. package/plugin/scripts/hook_activity.py +139 -0
  417. package/plugin/scripts/launch.py +94 -0
  418. package/plugin/scripts/logs.py +98 -0
  419. package/plugin/scripts/pairing.py +185 -0
  420. package/plugin/scripts/parcels.py +132 -0
  421. package/plugin/scripts/read-card.py +51 -0
  422. package/plugin/scripts/report.py +201 -0
  423. package/plugin/scripts/reset.py +180 -0
  424. package/plugin/scripts/roads.py +230 -0
  425. package/plugin/scripts/roles.py +274 -0
  426. package/plugin/scripts/runtime_log.py +69 -0
  427. package/plugin/scripts/runtime_processes.py +132 -0
  428. package/plugin/scripts/rutas.py +83 -0
  429. package/plugin/scripts/seat.py +1349 -0
  430. package/plugin/scripts/tokens.py +194 -0
  431. package/plugin/scripts/trust-repos.py +59 -0
  432. package/plugin/scripts/ui.py +280 -0
  433. package/plugin/scripts/units.py +80 -0
  434. package/plugin/scripts/workspace.py +364 -0
  435. package/plugin/skills/city/SKILL.md +191 -0
  436. package/templates/blank.md +39 -0
  437. package/templates/finance.md +44 -0
  438. package/templates/legal.md +45 -0
  439. package/templates/marketing.md +48 -0
  440. package/templates/product.md +45 -0
@@ -0,0 +1,67 @@
1
+ import { appendFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { CityContext } from '../city-config.js';
4
+ import { isoNow, randomId } from '../protocol.js';
5
+
6
+ export interface DiagnosticFields {
7
+ actor?: string;
8
+ mode?: string;
9
+ thread?: string;
10
+ command?: string;
11
+ outcome?: string;
12
+ message?: string;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ /** Append-only, secret-scrubbed diagnostics shared by hub and gateways. */
17
+ export function diagnosticLog(context: CityContext, component: string) {
18
+ const path = join(context.runtimeDir, 'diagnostics.jsonl');
19
+ return (event: string, fields: DiagnosticFields = {}): void => {
20
+ try {
21
+ mkdirSync(context.runtimeDir, { recursive: true, mode: 0o700 });
22
+ appendFileSync(
23
+ path,
24
+ JSON.stringify({
25
+ protocol: 'agents-city-diagnostic/1',
26
+ id: randomId('diagnostic'),
27
+ at: isoNow(),
28
+ pid: process.pid,
29
+ city: context.city.address,
30
+ component: clean(component, 80),
31
+ event: clean(event, 120),
32
+ ...scrub(fields),
33
+ }) + '\n',
34
+ { mode: 0o600 },
35
+ );
36
+ } catch {
37
+ // Diagnostics must never take the city down.
38
+ }
39
+ };
40
+ }
41
+
42
+ function scrub(fields: DiagnosticFields): Record<string, unknown> {
43
+ const out: Record<string, unknown> = {};
44
+ for (const [key, value] of Object.entries(fields)) {
45
+ if (/token|secret|authorization|credential/i.test(key)) {
46
+ out[key] = '[redacted]';
47
+ } else if (typeof value === 'string') {
48
+ out[key] = clean(
49
+ value
50
+ .replace(/([?&](?:token|secret|key)=)[^&\s]+/gi, '$1[redacted]')
51
+ .replace(/Bearer\s+\S+/gi, 'Bearer [redacted]')
52
+ .replace(/(Authorization:\s*)\S+(?:\s+\S+)?/gi, '$1[redacted]')
53
+ .replace(/(--(?:token|password|secret|api-key)(?:=|\s+))\S+/gi, '$1[redacted]'),
54
+ 2_000,
55
+ );
56
+ } else if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
57
+ out[key] = value;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function clean(value: unknown, max: number): string {
64
+ return String(value ?? '')
65
+ .trim()
66
+ .slice(0, max);
67
+ }
@@ -0,0 +1,109 @@
1
+ import { CommitteeFiles } from '../committee/storage.js';
2
+ import { DeliberationState, TERMINAL_STATUSES } from '../committee/types.js';
3
+ import { BusEnvelope } from '../protocol.js';
4
+
5
+ /**
6
+ * Decide whether a durable committee delivery still describes actionable
7
+ * state. This is intentionally used only while draining an offline actor's
8
+ * outbox: a transition's live notification remains useful even when that same
9
+ * transition has just advanced the state.
10
+ */
11
+ export function staleCommitteeEnvelopeReason(files: CommitteeFiles, envelope: BusEnvelope): string {
12
+ if (envelope.scope !== 'internal' || !envelope.kind.startsWith('committee.')) return '';
13
+ if (!envelope.thread) return 'committee delivery has no deliberation id';
14
+
15
+ let state: DeliberationState;
16
+ try {
17
+ state = files.load(envelope.thread);
18
+ } catch {
19
+ return 'deliberation state is unavailable';
20
+ }
21
+
22
+ if (TERMINAL_STATUSES.has(state.status)) return `deliberation is ${state.status}`;
23
+
24
+ const expected = expectedStatus(envelope.kind);
25
+ if (expected && !expected.includes(state.status)) {
26
+ return `delivery belongs to ${expected.join(' or ')}, current state is ${state.status}`;
27
+ }
28
+
29
+ if (envelope.kind === 'committee.assignment' && state.positions[envelope.to.actor]) {
30
+ return 'participant already submitted a position';
31
+ }
32
+ if (envelope.kind === 'committee.position_received') {
33
+ const deliveredCount = Number(envelope.payload.received);
34
+ const currentCount = Object.keys(state.positions).length;
35
+ if (!Number.isInteger(deliveredCount) || deliveredCount !== currentCount) {
36
+ return `position count advanced from ${deliveredCount || 0} to ${currentCount}`;
37
+ }
38
+ }
39
+ if (envelope.kind === 'committee.positions_ready') {
40
+ const currentCount = Object.keys(state.positions).length;
41
+ if (currentCount !== state.brief.participants.length) {
42
+ return 'the independent-position barrier is no longer complete';
43
+ }
44
+ }
45
+ if (envelope.kind === 'committee.floor.requested') {
46
+ const request = record(envelope.payload.request);
47
+ const requestId = String(request.id || '');
48
+ if (!state.floor.requests.some((item) => item.id === requestId && item.status === 'pending')) {
49
+ return 'floor request is no longer pending';
50
+ }
51
+ }
52
+ if (envelope.kind === 'committee.floor.granted') {
53
+ const requestId = String(envelope.payload.requestId || '');
54
+ if (
55
+ !state.floor.active ||
56
+ state.floor.active.requestId !== requestId ||
57
+ state.floor.active.actor !== envelope.to.actor
58
+ ) {
59
+ return 'floor grant is no longer active';
60
+ }
61
+ }
62
+ if (envelope.kind === 'committee.floor.denied') {
63
+ const requestId = String(envelope.payload.requestId || '');
64
+ if (!state.floor.requests.some((item) => item.id === requestId && item.status === 'denied')) {
65
+ return 'floor denial no longer matches the deliberation';
66
+ }
67
+ }
68
+ if (envelope.kind === 'committee.reply.received' || envelope.kind === 'committee.reply.heard') {
69
+ const reply = record(envelope.payload.reply);
70
+ const requestId = String(reply.requestId || '');
71
+ const actor = String(reply.actor || envelope.from.actor || '');
72
+ if (!state.floor.replies.some((item) => item.requestId === requestId && item.actor === actor)) {
73
+ return 'floor reply no longer matches the deliberation';
74
+ }
75
+ }
76
+ if (envelope.kind === 'committee.verification.assigned') {
77
+ const current = state.decisions.at(-1);
78
+ if (!current || current.verifier !== envelope.to.actor) {
79
+ return 'verification assignment is no longer current';
80
+ }
81
+ }
82
+
83
+ return '';
84
+ }
85
+
86
+ function expectedStatus(kind: string): DeliberationState['status'][] | null {
87
+ if (kind === 'committee.assignment' || kind === 'committee.position_received') {
88
+ return ['collecting'];
89
+ }
90
+ if (kind === 'committee.positions_ready') return ['review'];
91
+ if (
92
+ kind === 'committee.synthesis' ||
93
+ kind.startsWith('committee.floor.') ||
94
+ kind === 'committee.reply.received' ||
95
+ kind === 'committee.reply.heard'
96
+ ) {
97
+ return ['deliberating'];
98
+ }
99
+ if (kind === 'committee.verification.assigned') return ['verifying'];
100
+ if (kind === 'committee.verification.passed') return ['verified'];
101
+ if (kind === 'committee.verification.failed') return ['verification_failed'];
102
+ return null;
103
+ }
104
+
105
+ function record(value: unknown): Record<string, unknown> {
106
+ return value && typeof value === 'object' && !Array.isArray(value)
107
+ ? (value as Record<string, unknown>)
108
+ : {};
109
+ }
@@ -0,0 +1,66 @@
1
+ import { ActorRole, BUS_PROTOCOL, BusEnvelope, isoNow, randomId } from '../protocol.js';
2
+ import { CityContext } from '../city-config.js';
3
+ import { acknowledge, enqueueForActor, pendingForActor } from '../delivery-queue.js';
4
+ import { ConnectionRegistry } from './connections.js';
5
+
6
+ interface EnvelopeRouterOptions {
7
+ staleReason?: (envelope: BusEnvelope) => string;
8
+ onDrop?: (envelope: BusEnvelope, reason: string) => void;
9
+ }
10
+
11
+ export function envelopeRouter(
12
+ context: CityContext,
13
+ connections: ConnectionRegistry,
14
+ options: EnvelopeRouterOptions = {},
15
+ ) {
16
+ const internal = (
17
+ kind: string,
18
+ fromActor: string,
19
+ fromRole: ActorRole,
20
+ toActor: string,
21
+ thread: string | null,
22
+ payload: Record<string, unknown>,
23
+ ): BusEnvelope => {
24
+ const envelope: BusEnvelope = {
25
+ protocol: BUS_PROTOCOL,
26
+ id: randomId('msg'),
27
+ kind,
28
+ scope: 'internal',
29
+ thread,
30
+ from: { city: context.city.address, actor: fromActor, role: fromRole },
31
+ to: { city: context.city.address, actor: toActor },
32
+ createdAt: isoNow(),
33
+ payload,
34
+ };
35
+ enqueueForActor(context.runtimeDir, envelope);
36
+ connections.deliver(toActor, envelope);
37
+ return envelope;
38
+ };
39
+
40
+ const roadInbound = (envelope: BusEnvelope): void => {
41
+ enqueueForActor(context.runtimeDir, envelope);
42
+ connections.deliver('seat', envelope);
43
+ };
44
+
45
+ const drain = (actor: string): number => {
46
+ const pending = pendingForActor(context.runtimeDir, actor);
47
+ let delivered = 0;
48
+ for (const envelope of pending) {
49
+ const reason = options.staleReason?.(envelope) || '';
50
+ if (reason) {
51
+ acknowledge(context.runtimeDir, actor, envelope.id);
52
+ options.onDrop?.(envelope, reason);
53
+ continue;
54
+ }
55
+ if (connections.deliver(actor, envelope)) delivered += 1;
56
+ }
57
+ return delivered;
58
+ };
59
+
60
+ const ack = (actor: string, envelopeId: string): boolean =>
61
+ acknowledge(context.runtimeDir, actor, envelopeId);
62
+
63
+ return { internal, roadInbound, drain, ack };
64
+ }
65
+
66
+ export type EnvelopeRouter = ReturnType<typeof envelopeRouter>;
@@ -0,0 +1,91 @@
1
+ import {
2
+ closeSync,
3
+ mkdirSync,
4
+ openSync,
5
+ readFileSync,
6
+ statSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from 'fs';
10
+ import { join } from 'path';
11
+ import { CityContext } from '../city-config.js';
12
+ import { HubEndpoint } from '../protocol.js';
13
+ import { atomicJson, endpointPath } from '../runtime-files.js';
14
+
15
+ export function acquireHub(context: CityContext): () => void {
16
+ mkdirSync(context.runtimeDir, { recursive: true, mode: 0o700 });
17
+ const lock = join(context.runtimeDir, 'hub.lock');
18
+ const owner = process.pid;
19
+ let acquired = false;
20
+ for (let attempt = 0; attempt < 2 && !acquired; attempt += 1) {
21
+ try {
22
+ const fd = openSync(lock, 'wx', 0o600);
23
+ try {
24
+ writeFileSync(fd, String(owner) + '\n');
25
+ } finally {
26
+ closeSync(fd);
27
+ }
28
+ acquired = true;
29
+ } catch (error) {
30
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
31
+ const oldPid = lockOwner(lock);
32
+ if (oldPid > 0) {
33
+ if (processAlive(oldPid)) {
34
+ throw new Error(`city bus is already running as pid ${oldPid}`);
35
+ }
36
+ } else if (lockIsFresh(lock)) {
37
+ // Another process may have created the file and not written its PID yet.
38
+ // Never unlink a fresh, unreadable lock: that was the race that let two
39
+ // detached hubs survive one concurrent first start.
40
+ throw new Error('city bus is already starting');
41
+ }
42
+ try {
43
+ unlinkSync(lock);
44
+ } catch (unlinkError) {
45
+ if ((unlinkError as NodeJS.ErrnoException).code !== 'ENOENT') throw unlinkError;
46
+ }
47
+ }
48
+ }
49
+ if (!acquired) throw new Error('could not acquire the city bus lock');
50
+ return () => {
51
+ // An old process must never erase a replacement hub's endpoint or lock.
52
+ if (lockOwner(lock) !== owner) return;
53
+ const endpoint = endpointPath(context);
54
+ try {
55
+ const published = JSON.parse(readFileSync(endpoint, 'utf8')) as HubEndpoint;
56
+ if (published.pid === owner) unlinkSync(endpoint);
57
+ } catch {}
58
+ try {
59
+ unlinkSync(lock);
60
+ } catch {}
61
+ };
62
+ }
63
+
64
+ function lockOwner(path: string): number {
65
+ try {
66
+ return Number(readFileSync(path, 'utf8').trim()) || 0;
67
+ } catch {
68
+ return 0;
69
+ }
70
+ }
71
+
72
+ function lockIsFresh(path: string): boolean {
73
+ try {
74
+ return Date.now() - statSync(path).mtimeMs < 5_000;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ function processAlive(pid: number): boolean {
81
+ try {
82
+ process.kill(pid, 0);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ export function publishEndpoint(context: CityContext, endpoint: HubEndpoint): void {
90
+ atomicJson(endpointPath(context), endpoint);
91
+ }
@@ -0,0 +1,89 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import WebSocket from 'ws';
4
+ import { CityContext, runtimeDirForCity } from '../city-config.js';
5
+ import { queueRoad } from '../delivery-queue.js';
6
+ import { BusEnvelope, HubEndpoint, Road, randomId } from '../protocol.js';
7
+
8
+ export async function sendLocalRoad(
9
+ context: CityContext,
10
+ road: Road,
11
+ envelope: BusEnvelope,
12
+ ): Promise<string> {
13
+ const destinationRuntime = runtimeDirForCity(context.appHome, road.id);
14
+ let endpoint: HubEndpoint;
15
+ try {
16
+ endpoint = JSON.parse(
17
+ readFileSync(join(destinationRuntime, 'endpoint.json'), 'utf8'),
18
+ ) as HubEndpoint;
19
+ if (endpoint.cityId !== road.id || endpoint.cityAddress !== road.address)
20
+ throw new Error('identity mismatch');
21
+ } catch {
22
+ queueRoad(destinationRuntime, envelope);
23
+ return `${road.address} is offline: queued on the local bus`;
24
+ }
25
+ try {
26
+ return await deliver(endpoint, context.city.address, envelope);
27
+ } catch {
28
+ queueRoad(destinationRuntime, envelope);
29
+ return `${road.address} became unavailable: queued on the local bus`;
30
+ }
31
+ }
32
+
33
+ export function localRoadOnline(context: CityContext, road: Road): boolean {
34
+ try {
35
+ const endpoint = JSON.parse(
36
+ readFileSync(join(runtimeDirForCity(context.appHome, road.id), 'endpoint.json'), 'utf8'),
37
+ ) as HubEndpoint;
38
+ if (endpoint.cityId !== road.id || endpoint.cityAddress !== road.address || endpoint.pid <= 0)
39
+ return false;
40
+ process.kill(endpoint.pid, 0);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function deliver(endpoint: HubEndpoint, from: string, envelope: BusEnvelope): Promise<string> {
48
+ return new Promise((resolve, reject) => {
49
+ const url = new URL(endpoint.url);
50
+ url.searchParams.set('mode', 'road');
51
+ url.searchParams.set('from', from);
52
+ url.searchParams.set('token', endpoint.roadToken);
53
+ const ws = new WebSocket(url);
54
+ const requestId = randomId('request');
55
+ const timer = setTimeout(() => finish(new Error('local road timed out')), 5_000);
56
+ let sent = false;
57
+ let settled = false;
58
+
59
+ const finish = (error?: Error): void => {
60
+ if (settled) return;
61
+ settled = true;
62
+ clearTimeout(timer);
63
+ try {
64
+ ws.close();
65
+ } catch {}
66
+ if (error) reject(error);
67
+ else resolve(`delivered locally to ${endpoint.cityAddress}`);
68
+ };
69
+ ws.on('message', (raw) => {
70
+ let message: Record<string, unknown>;
71
+ try {
72
+ message = JSON.parse(String(raw));
73
+ } catch {
74
+ return;
75
+ }
76
+ if (message.type === 'welcome' && !sent) {
77
+ sent = true;
78
+ ws.send(JSON.stringify({ type: 'road.ingress', requestId, envelope }));
79
+ } else if (message.type === 'result' && message.requestId === requestId) {
80
+ if (message.ok) finish();
81
+ else finish(new Error(String(message.error || 'local road refused the message')));
82
+ }
83
+ });
84
+ ws.on('error', () => finish(new Error('local road connection failed')));
85
+ ws.on('close', () => {
86
+ if (!sent) finish(new Error('local road closed before delivery'));
87
+ });
88
+ });
89
+ }
@@ -0,0 +1,165 @@
1
+ import WebSocket from 'ws';
2
+ import { CityContext } from '../city-config.js';
3
+ import { BUS_PROTOCOL, BusEnvelope, isoNow, randomId } from '../protocol.js';
4
+
5
+ export function remoteRoadBridge(context: CityContext, receive: (envelope: BusEnvelope) => void) {
6
+ const base = process.env.CITY_BUS_URL || '';
7
+ const token = process.env.CITY_BUS_TOKEN || '';
8
+ const enabled = Boolean(base && token);
9
+ let ws: WebSocket | null = null;
10
+ let online = false;
11
+ let stopped = false;
12
+ let backoff = 1_000;
13
+ let roster = new Set<string>();
14
+ let tail: Promise<string> = Promise.resolve('');
15
+
16
+ const connect = (): void => {
17
+ if (!enabled || stopped) return;
18
+ const url = new URL('/ws', base);
19
+ url.protocol = url.protocol.replace(/^http/, 'ws');
20
+ url.searchParams.set('agent', context.city.address);
21
+ ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` } });
22
+ ws.on('open', () => {
23
+ online = true;
24
+ backoff = 1_000;
25
+ });
26
+ ws.on('message', (raw) => handleMessage(String(raw)));
27
+ ws.on('close', () => {
28
+ online = false;
29
+ if (!stopped) {
30
+ setTimeout(connect, backoff);
31
+ backoff = Math.min(backoff * 2, 30_000);
32
+ }
33
+ });
34
+ ws.on('error', () => {});
35
+ };
36
+
37
+ const handleMessage = (raw: string): void => {
38
+ let message: Record<string, unknown>;
39
+ try {
40
+ message = JSON.parse(raw);
41
+ } catch {
42
+ return;
43
+ }
44
+ if (message.type === 'welcome' || message.type === 'roster') {
45
+ const entries = (message.roster || message.agents || []) as Array<{ agent?: string }>;
46
+ roster = new Set(entries.map((entry) => entry.agent || '').filter(Boolean));
47
+ return;
48
+ }
49
+ if (message.type === 'presence') {
50
+ const address = String(message.agent || '');
51
+ if (message.status === 'online') roster.add(address);
52
+ else roster.delete(address);
53
+ return;
54
+ }
55
+ if (message.type !== 'msg') return;
56
+ const from = String(message.from || '');
57
+ const road = context.roads.find((candidate) => !candidate.local && candidate.address === from);
58
+ if (!road) return;
59
+ const candidate = message.envelope as BusEnvelope | undefined;
60
+ let envelope: BusEnvelope;
61
+ if (candidate !== undefined) {
62
+ if (
63
+ candidate.protocol !== BUS_PROTOCOL ||
64
+ candidate.scope !== 'road' ||
65
+ candidate.from.city !== from ||
66
+ candidate.from.actor !== 'seat' ||
67
+ candidate.to.city !== context.city.address ||
68
+ candidate.to.actor !== 'seat'
69
+ ) {
70
+ return;
71
+ }
72
+ envelope = candidate;
73
+ } else {
74
+ envelope = legacyEnvelope(
75
+ context,
76
+ from,
77
+ String(message.text || ''),
78
+ String(message.msg_id || randomId('remote')),
79
+ );
80
+ }
81
+ if (envelope.scope === 'road' && envelope.to.city === context.city.address) receive(envelope);
82
+ };
83
+
84
+ const sendRaw = (to: string, envelope: BusEnvelope): Promise<string> =>
85
+ new Promise((resolve, reject) => {
86
+ if (!enabled) return reject(new Error('no remote road transport is configured'));
87
+ if (!online || !ws || ws.readyState !== WebSocket.OPEN) {
88
+ return reject(new Error('the remote road is not connected'));
89
+ }
90
+ const requestId = randomId('remote_request');
91
+ const onMessage = (raw: WebSocket.RawData): void => {
92
+ let message: Record<string, unknown>;
93
+ try {
94
+ message = JSON.parse(String(raw));
95
+ } catch {
96
+ return;
97
+ }
98
+ if (!['sent', 'queued', 'error'].includes(String(message.type))) return;
99
+ if (message.request_id && message.request_id !== requestId) return;
100
+ cleanup();
101
+ if (message.type === 'error')
102
+ reject(new Error(String(message.error || 'remote road refused the message')));
103
+ else if (message.type === 'queued') resolve(`${to} is offline: queued remotely`);
104
+ else resolve(`delivered remotely to ${to}`);
105
+ };
106
+ const cleanup = (): void => {
107
+ clearTimeout(timer);
108
+ ws?.off('message', onMessage);
109
+ };
110
+ const timer = setTimeout(() => {
111
+ cleanup();
112
+ reject(new Error('the remote road did not answer in 10s'));
113
+ }, 10_000);
114
+ ws.on('message', onMessage);
115
+ ws.send(
116
+ JSON.stringify({
117
+ type: 'send',
118
+ request_id: requestId,
119
+ to,
120
+ text: String(envelope.payload.text || ''),
121
+ envelope,
122
+ }),
123
+ );
124
+ });
125
+
126
+ const send = (to: string, envelope: BusEnvelope): Promise<string> => {
127
+ const turn = tail.then(
128
+ () => sendRaw(to, envelope),
129
+ () => sendRaw(to, envelope),
130
+ );
131
+ tail = turn.catch(() => '');
132
+ return turn;
133
+ };
134
+
135
+ const close = (): void => {
136
+ stopped = true;
137
+ try {
138
+ ws?.close();
139
+ } catch {}
140
+ };
141
+
142
+ return {
143
+ start: connect,
144
+ send,
145
+ close,
146
+ enabled: () => enabled,
147
+ online: (address: string) => roster.has(address),
148
+ };
149
+ }
150
+
151
+ function legacyEnvelope(context: CityContext, from: string, body: string, id: string): BusEnvelope {
152
+ return {
153
+ protocol: BUS_PROTOCOL,
154
+ id,
155
+ kind: 'road.message',
156
+ scope: 'road',
157
+ thread: null,
158
+ from: { city: from, actor: 'seat', role: 'external-seat' },
159
+ to: { city: context.city.address, actor: 'seat' },
160
+ createdAt: isoNow(),
161
+ payload: { text: body, legacy: true },
162
+ };
163
+ }
164
+
165
+ export type RemoteRoadBridge = ReturnType<typeof remoteRoadBridge>;
@@ -0,0 +1,99 @@
1
+ import { ActorRole, BUS_PROTOCOL, BusEnvelope, isoNow, randomId, text } from '../protocol.js';
2
+ import { CityContext } from '../city-config.js';
3
+ import { recordRoadInbox, takeRoadInbox } from '../delivery-queue.js';
4
+ import { requireChair } from '../committee/guards.js';
5
+ import { wrapUntrusted } from '../untrusted.js';
6
+ import { EnvelopeRouter } from './envelopes.js';
7
+ import { localRoadOnline, sendLocalRoad } from './local-roads.js';
8
+ import { remoteRoadBridge } from './remote-roads.js';
9
+
10
+ export function roadController(context: CityContext, router: EnvelopeRouter) {
11
+ const inbound = (envelope: BusEnvelope): void => {
12
+ const road = context.roads.find((candidate) => candidate.address === envelope.from.city);
13
+ if (!road) throw new Error(`there is no road from ${envelope.from.city}`);
14
+ if (
15
+ envelope.protocol !== BUS_PROTOCOL ||
16
+ envelope.scope !== 'road' ||
17
+ envelope.from.actor !== 'seat' ||
18
+ envelope.to.city !== context.city.address ||
19
+ envelope.to.actor !== 'seat'
20
+ ) {
21
+ throw new Error('invalid road envelope');
22
+ }
23
+ // The body is untrusted text from another city: wrap it in an unforgeable
24
+ // boundary and defang chat-template tokens before it can reach the seat's
25
+ // context. Done once, here, so every inbound path (local and remote) and
26
+ // the durable inbox all carry the guarded form. A non-string payload text
27
+ // is left untouched; only the string body is wrapped.
28
+ const rawBody = envelope.payload?.text;
29
+ const guarded =
30
+ typeof rawBody === 'string'
31
+ ? {
32
+ ...envelope,
33
+ payload: {
34
+ ...envelope.payload,
35
+ text: wrapUntrusted(rawBody, envelope.from.city).text,
36
+ textRaw: undefined,
37
+ },
38
+ }
39
+ : envelope;
40
+ recordRoadInbox(context.runtimeDir, guarded);
41
+ router.roadInbound(guarded);
42
+ };
43
+ const remote = remoteRoadBridge(context, (envelope) => {
44
+ try {
45
+ inbound(envelope);
46
+ } catch (error) {
47
+ console.error(`[city-bus] dropped remote envelope: ${(error as Error).message}`);
48
+ }
49
+ });
50
+
51
+ const sendOne = async (to: string, body: string): Promise<string> => {
52
+ const road = context.roads.find((candidate) => candidate.address === to);
53
+ if (!road) throw new Error(`no road from ${context.city.address} to ${to}`);
54
+ const envelope: BusEnvelope = {
55
+ protocol: BUS_PROTOCOL,
56
+ id: randomId('msg'),
57
+ kind: 'road.message',
58
+ scope: 'road',
59
+ thread: null,
60
+ from: { city: context.city.address, actor: 'seat', role: 'chair' },
61
+ to: { city: to, actor: 'seat' },
62
+ createdAt: isoNow(),
63
+ payload: { text: body, trust: 'information-not-authority' },
64
+ };
65
+ return road.local ? sendLocalRoad(context, road, envelope) : remote.send(to, envelope);
66
+ };
67
+
68
+ const command = async (
69
+ name: string,
70
+ payload: Record<string, unknown>,
71
+ actor: string,
72
+ role: ActorRole,
73
+ ): Promise<unknown> => {
74
+ if (name === 'road.roster') {
75
+ requireChair(actor, role);
76
+ return context.roads.map((road) => ({
77
+ ...road,
78
+ online: road.local ? localRoadOnline(context, road) : remote.online(road.address),
79
+ }));
80
+ }
81
+ if (name === 'road.inbox') {
82
+ requireChair(actor, role);
83
+ return takeRoadInbox(context.runtimeDir);
84
+ }
85
+ if (name !== 'road.send') throw new Error(`unknown road command: ${name}`);
86
+ requireChair(actor, role);
87
+ const to = text(payload.to, 'to');
88
+ const body = text(payload.text, 'text');
89
+ if (to !== '*') return { results: [await sendOne(to, body)] };
90
+ if (!context.roads.length) throw new Error('this city has no roads');
91
+ const results: string[] = [];
92
+ for (const road of context.roads) results.push(await sendOne(road.address, body));
93
+ return { results };
94
+ };
95
+
96
+ return { command, inbound, start: remote.start, close: remote.close };
97
+ }
98
+
99
+ export type RoadController = ReturnType<typeof roadController>;