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,531 @@
1
+ #!/usr/bin/env python3
2
+ """The credential broker: repo windows ask for actions, never hold the keys.
3
+
4
+ A caged window cannot read the `gh` token — that is the cage doing its job.
5
+ But opening a pull request is legitimate work. So the window asks this broker,
6
+ a small process the owner runs OUTSIDE every cage, and the broker performs a
7
+ narrow, validated action with the real credentials:
8
+
9
+ broker.py serve --data <city-dir> the owner's side, unjailed
10
+ broker.py mint --data <city-dir> <actor> --repo <path>
11
+ broker.py call pr --title T [--body B] the window's side, jailed
12
+ broker.py call push
13
+ broker.py verify --data <city-dir> walk the audit chain
14
+ broker.py stop --data <city-dir>
15
+
16
+ The token names one window and is bound to ONE repo at mint time: a stolen
17
+ token cannot choose a different target, and the server refuses work on the
18
+ default branch outright. Only the SHA-256 of each token is stored. Every
19
+ request — served or refused — lands in an audit log where each line carries
20
+ the hash of the previous one, so a rewritten history no longer verifies.
21
+
22
+ Deliberately boring transport: HTTP on 127.0.0.1, stdlib only, one request at
23
+ a time. Broker actions are rare; a queue is the correct behaviour.
24
+ """
25
+
26
+ import argparse
27
+ import hashlib
28
+ import hmac
29
+ import json
30
+ import os
31
+ import secrets
32
+ import signal
33
+ import socketserver
34
+ import subprocess
35
+ import sys
36
+ import time
37
+ import urllib.error
38
+ import urllib.request
39
+ from http.server import BaseHTTPRequestHandler, HTTPServer
40
+
41
+ import rutas
42
+ from evidencia import Evidencia
43
+
44
+ MAX_CUERPO = 64_000
45
+ MAX_RESPUESTA = 256_000
46
+ RAMAS_PROTEGIDAS = ('main', 'master')
47
+
48
+
49
+ def estado_de(datos):
50
+ """The runtime state dir for one city's broker, derived from the data dir."""
51
+ real = rutas.canonicaliza(datos)
52
+ marca = hashlib.sha256(real.encode()).hexdigest()[:12]
53
+ casa = rutas.canonicaliza(os.environ.get('AGENTS_CITY_HOME') or '~/.agents-city')
54
+ ruta = os.path.join(casa, '.runtime', 'broker', marca)
55
+ os.makedirs(ruta, mode=0o700, exist_ok=True)
56
+ return ruta
57
+
58
+
59
+ def _lee_json(ruta, defecto):
60
+ try:
61
+ with open(ruta, encoding='utf-8') as f:
62
+ return json.load(f)
63
+ except (OSError, json.JSONDecodeError):
64
+ return defecto
65
+
66
+
67
+ def _escribe_json(ruta, objeto):
68
+ # Atomic: write a sibling temp then rename, so a crash mid-write never
69
+ # leaves a half-written token or secret store behind.
70
+ tmp = f'{ruta}.tmp'
71
+ with open(tmp, 'w', encoding='utf-8') as f:
72
+ json.dump(objeto, f, indent=2)
73
+ os.chmod(tmp, 0o600)
74
+ os.replace(tmp, ruta)
75
+
76
+
77
+ # ── the audit chain ──────────────────────────────────────────────────────────
78
+
79
+
80
+ def audita(estado, entrada):
81
+ """Append one entry; each line carries the SHA-256 of the previous line."""
82
+ ruta = os.path.join(estado, 'audit.log')
83
+ previa = 'genesis'
84
+ try:
85
+ with open(ruta, 'rb') as f:
86
+ lineas = f.read().splitlines()
87
+ if lineas:
88
+ previa = hashlib.sha256(lineas[-1]).hexdigest()
89
+ except OSError:
90
+ pass
91
+ # Every line states what it proves, not just whether it succeeded: the hash
92
+ # chain guards against tampering, the evidence state guards against a
93
+ # missing check reading as a pass. `unknown` never means allowed.
94
+ entrada.setdefault('evidence', str(Evidencia.DESCONOCIDO))
95
+ entrada = dict(entrada, ts=time.strftime('%Y-%m-%dT%H:%M:%S%z'), prev=previa)
96
+ with open(ruta, 'a', encoding='utf-8') as f:
97
+ f.write(json.dumps(entrada, sort_keys=True) + '\n')
98
+ os.chmod(ruta, 0o600)
99
+
100
+
101
+ def verifica(estado):
102
+ """Walk the chain; return the first broken line number, or 0 when intact."""
103
+ ruta = os.path.join(estado, 'audit.log')
104
+ try:
105
+ with open(ruta, 'rb') as f:
106
+ lineas = f.read().splitlines()
107
+ except OSError:
108
+ return 0
109
+ esperada = 'genesis'
110
+ for n, cruda in enumerate(lineas, 1):
111
+ try:
112
+ fila = json.loads(cruda)
113
+ except json.JSONDecodeError:
114
+ return n
115
+ if fila.get('prev') != esperada:
116
+ return n
117
+ esperada = hashlib.sha256(cruda).hexdigest()
118
+ return 0
119
+
120
+
121
+ # ── tokens ───────────────────────────────────────────────────────────────────
122
+
123
+
124
+ def acuna(datos, actor, repo, solo_fichero=False):
125
+ """Mint one token bound to (actor, repo); store only its hash."""
126
+ repo = rutas.canonicaliza(repo)
127
+ if not os.path.isdir(os.path.join(repo, '.git')):
128
+ raise ValueError(f'not a git repo: {repo}')
129
+ estado = estado_de(datos)
130
+ token = 'cb_' + secrets.token_hex(24)
131
+ hash_ = hashlib.sha256(token.encode()).hexdigest()
132
+ tokens = _lee_json(os.path.join(estado, 'tokens.json'), {})
133
+ tokens = {h: v for h, v in tokens.items() if v.get('actor') != actor}
134
+ tokens[hash_] = {'actor': actor, 'repo': repo}
135
+ _escribe_json(os.path.join(estado, 'tokens.json'), tokens)
136
+ carpeta = os.path.join(estado, 'tokens')
137
+ os.makedirs(carpeta, mode=0o700, exist_ok=True)
138
+ fichero = os.path.join(carpeta, f'{actor}.token')
139
+ with open(fichero, 'w', encoding='utf-8') as f:
140
+ f.write(token)
141
+ os.chmod(fichero, 0o600)
142
+ audita(estado, {'verb': 'mint', 'actor': actor, 'repo': repo, 'ok': True,
143
+ 'evidence': str(Evidencia.IMPUESTO)})
144
+ return fichero if solo_fichero else token
145
+
146
+
147
+ def identidad_de(estado, cabecera):
148
+ """Authorization header -> {actor, repo}, or None. Constant-time compare."""
149
+ if not cabecera or not cabecera.startswith('Bearer '):
150
+ return None
151
+ hash_ = hashlib.sha256(cabecera[7:].strip().encode()).hexdigest()
152
+ for guardado, quien in _lee_json(os.path.join(estado, 'tokens.json'), {}).items():
153
+ if hmac.compare_digest(guardado, hash_):
154
+ return quien
155
+ return None
156
+
157
+
158
+ # ── host-bound secrets ───────────────────────────────────────────────────────
159
+ # The agent never holds the secret, not even ciphertext. It asks the broker to
160
+ # make an outbound call carrying a named secret, and the broker injects the
161
+ # value only when the target host matches one the owner bound to that name —
162
+ # exact hostname, no wildcards, no suffix or port games. A leaked transcript or
163
+ # `ps` line therefore contains the request, never the credential.
164
+
165
+
166
+ def _secretos_ruta(estado):
167
+ return os.path.join(estado, 'secrets.json')
168
+
169
+
170
+ def guarda_secreto(datos, clave, valor, hosts):
171
+ """Owner side: bind a secret value to an exact-match host allowlist."""
172
+ limpios = [h.strip().lower() for h in hosts if h.strip()]
173
+ if not limpios:
174
+ raise ValueError('a secret needs at least one --allow-host')
175
+ for h in limpios:
176
+ if '*' in h or '/' in h or ':' in h:
177
+ raise ValueError(f'invalid host "{h}": exact hostnames only, no wildcards or ports')
178
+ estado = estado_de(datos)
179
+ tienda = _lee_json(_secretos_ruta(estado), {})
180
+ tienda[clave] = {'value': valor, 'hosts': sorted(set(limpios))}
181
+ _escribe_json(_secretos_ruta(estado), tienda)
182
+ audita(estado, {'verb': 'secret.set', 'actor': 'owner', 'ok': True,
183
+ 'detail': clave, 'evidence': str(Evidencia.IMPUESTO)})
184
+ return limpios
185
+
186
+
187
+ def _host_permitido(estado, clave, host):
188
+ """(value, None) when host is bound to the secret, else (None, reason)."""
189
+ tienda = _lee_json(_secretos_ruta(estado), {})
190
+ entrada = tienda.get(clave)
191
+ if not entrada:
192
+ return None, f'no secret named "{clave}"'
193
+ objetivo = (host or '').strip().lower()
194
+ if objetivo not in set(entrada.get('hosts', [])):
195
+ return None, f'"{clave}" is not bound to host "{objetivo}"'
196
+ return entrada['value'], None
197
+
198
+
199
+ # ── the actions the broker is willing to perform ─────────────────────────────
200
+
201
+
202
+ class _NoSigue(urllib.request.HTTPRedirectHandler):
203
+ """A redirect handler that refuses every redirect.
204
+
205
+ Returning None from redirect_request tells urllib not to follow the 3xx;
206
+ the response is handed back to the caller as-is. This is what keeps the
207
+ injected secret from ever leaving the host it was bound to."""
208
+
209
+ def redirect_request(self, *_args, **_kwargs):
210
+ return None
211
+
212
+
213
+ #: One shared opener with redirects disabled. Built once, not per request.
214
+ _SIN_REDIRECCIONES = urllib.request.build_opener(_NoSigue)
215
+
216
+
217
+ def _tiene_control(texto):
218
+ """True if the string carries a control character (CR/LF/NUL/etc.)."""
219
+ return any(ord(c) < 0x20 or ord(c) == 0x7F for c in texto)
220
+
221
+
222
+ def _git(repo, *args):
223
+ r = subprocess.run(['git', '-C', repo, *args], capture_output=True, text=True)
224
+ return r.returncode, (r.stdout + r.stderr).strip()
225
+
226
+
227
+ def _rama_actual(repo):
228
+ codigo, rama = _git(repo, 'symbolic-ref', '--short', 'HEAD')
229
+ return rama if codigo == 0 else ''
230
+
231
+
232
+ def _rechaza_rama(repo):
233
+ """The one rule that never bends: no broker action lands on the default branch."""
234
+ rama = _rama_actual(repo)
235
+ if not rama:
236
+ return 'refused: detached HEAD'
237
+ if rama in RAMAS_PROTEGIDAS:
238
+ return f'refused: "{rama}" is the default branch'
239
+ codigo, salida = _git(repo, 'symbolic-ref', 'refs/remotes/origin/HEAD')
240
+ if codigo == 0 and salida.rsplit('/', 1)[-1] == rama:
241
+ return f'refused: "{rama}" is the default branch'
242
+ return ''
243
+
244
+
245
+ def accion_push(quien, _cuerpo, _estado):
246
+ motivo = _rechaza_rama(quien['repo'])
247
+ if motivo:
248
+ return 422, {'error': motivo}
249
+ codigo, salida = _git(quien['repo'], 'push', 'origin', 'HEAD')
250
+ if codigo != 0:
251
+ return 502, {'error': f'git push failed: {salida[-500:]}'}
252
+ return 200, {'ok': True, 'repo': quien['repo'], 'branch': _rama_actual(quien['repo'])}
253
+
254
+
255
+ def accion_pr(quien, cuerpo, _estado):
256
+ titulo = str(cuerpo.get('title') or '').strip()
257
+ if not titulo:
258
+ return 422, {'error': 'a pull request needs a title'}
259
+ motivo = _rechaza_rama(quien['repo'])
260
+ if motivo:
261
+ return 422, {'error': motivo}
262
+ orden = ['gh', 'pr', 'create', '--title', titulo, '--body', str(cuerpo.get('body') or '')]
263
+ if cuerpo.get('base'):
264
+ orden += ['--base', str(cuerpo['base'])]
265
+ r = subprocess.run(orden, cwd=quien['repo'], capture_output=True, text=True)
266
+ if r.returncode != 0:
267
+ return 502, {'error': f'gh pr create failed: {(r.stdout + r.stderr).strip()[-500:]}'}
268
+ return 200, {'ok': True, 'repo': quien['repo'], 'url': r.stdout.strip()}
269
+
270
+
271
+ def accion_fetch(quien, cuerpo, estado):
272
+ """Make one outbound HTTPS call, injecting a named secret only toward a
273
+ host the owner bound to it. The window supplies host/path/method and the
274
+ secret's NAME; the value is looked up here and never returned."""
275
+ clave = str(cuerpo.get('secret') or '').strip()
276
+ host = str(cuerpo.get('host') or '').strip()
277
+ metodo = str(cuerpo.get('method') or 'GET').upper()
278
+ if not clave or not host:
279
+ return 422, {'error': 'fetch needs "secret" and "host"'}
280
+ if metodo not in ('GET', 'POST'):
281
+ return 422, {'error': f'unsupported method "{metodo}"'}
282
+ valor, motivo = _host_permitido(estado, clave, host)
283
+ if motivo:
284
+ return 403, {'error': motivo}
285
+ cabecera = str(cuerpo.get('header') or 'Authorization')
286
+ plantilla = str(cuerpo.get('template') or 'Bearer {secret}')
287
+ ruta = str(cuerpo.get('path') or '/')
288
+ if not ruta.startswith('/'):
289
+ return 422, {'error': 'path must be absolute (start with /)'}
290
+ # Window-controlled strings must not smuggle CR/LF or other control bytes
291
+ # into the request line or headers: reject them before urllib raises deep
292
+ # inside the handler stack, where it would escape as a 500.
293
+ if any(_tiene_control(s) for s in (host, ruta, cabecera, plantilla)):
294
+ return 422, {'error': 'control characters are not allowed in a fetch request'}
295
+ url = f'https://{host}{ruta}'
296
+ datos = cuerpo.get('body')
297
+ peticion = urllib.request.Request(
298
+ url, method=metodo,
299
+ data=json.dumps(datos).encode() if datos is not None else None,
300
+ headers={cabecera: plantilla.replace('{secret}', str(valor)),
301
+ 'content-type': 'application/json'})
302
+ try:
303
+ # _SIN_REDIRECCIONES never follows a 3xx, so the injected secret is
304
+ # sent only to the bound host — a redirect to an unbound (or http://)
305
+ # target is returned as-is, not chased with the credential attached.
306
+ with _SIN_REDIRECCIONES.open(peticion, timeout=60) as r: # noqa: S310 (https + no-redirect)
307
+ texto = r.read(MAX_RESPUESTA).decode('utf-8', 'replace')
308
+ return 200, {'ok': True, 'status': r.status, 'host': host, 'body': texto}
309
+ except urllib.error.HTTPError as e:
310
+ cuerpo_err = e.read(MAX_RESPUESTA).decode('utf-8', 'replace')
311
+ return 200, {'ok': True, 'status': e.code, 'host': host, 'body': cuerpo_err}
312
+ except (urllib.error.URLError, OSError, ValueError) as e:
313
+ return 502, {'error': f'fetch failed: {str(e)[-300:]}'}
314
+
315
+
316
+ ACCIONES = {'/v1/push': accion_push, '/v1/pr': accion_pr, '/v1/fetch': accion_fetch}
317
+
318
+
319
+ # ── the server ───────────────────────────────────────────────────────────────
320
+
321
+
322
+ class Manejador(BaseHTTPRequestHandler):
323
+ estado = ''
324
+
325
+ def log_message(self, *_args):
326
+ pass
327
+
328
+ def _responde(self, codigo, objeto):
329
+ cuerpo = (json.dumps(objeto) + '\n').encode()
330
+ self.send_response(codigo)
331
+ self.send_header('content-type', 'application/json')
332
+ self.send_header('content-length', str(len(cuerpo)))
333
+ self.end_headers()
334
+ self.wfile.write(cuerpo)
335
+
336
+ def do_GET(self):
337
+ if self.path == '/health':
338
+ return self._responde(200, {'ok': True})
339
+ return self._responde(404, {'error': 'not found'})
340
+
341
+ def do_POST(self):
342
+ accion = ACCIONES.get(self.path)
343
+ if not accion:
344
+ return self._responde(404, {'error': 'not found'})
345
+ quien = identidad_de(self.estado, self.headers.get('Authorization'))
346
+ if not quien:
347
+ # No binding could be established: unknown evidence, never allowed.
348
+ audita(self.estado, {'verb': self.path, 'actor': None, 'ok': False,
349
+ 'detail': 'unrecognised token',
350
+ 'evidence': str(Evidencia.DESCONOCIDO)})
351
+ return self._responde(401 if not self.headers.get('Authorization') else 403,
352
+ {'error': 'token not recognised'})
353
+ try:
354
+ largo = min(int(self.headers.get('Content-Length') or 0), MAX_CUERPO)
355
+ cuerpo = json.loads(self.rfile.read(largo) or b'{}')
356
+ except (ValueError, json.JSONDecodeError):
357
+ return self._responde(400, {'error': 'invalid json'})
358
+ try:
359
+ codigo, respuesta = accion(quien, cuerpo, self.estado)
360
+ except Exception as e: # noqa: BLE001 — a handler must never leak a 500 with a stack trace
361
+ audita(self.estado, {'verb': self.path, 'actor': quien['actor'],
362
+ 'repo': quien['repo'], 'ok': False, 'detail': 'action raised',
363
+ 'evidence': str(Evidencia.DESCONOCIDO)})
364
+ return self._responde(500, {'error': f'action failed: {str(e)[-200:]}'})
365
+ # A control that decided the outcome — allow or refuse — is `enforced`.
366
+ # A transport failure (502) reached no decision, so it proves nothing:
367
+ # that is `unknown`, never `enforced`.
368
+ ev = Evidencia.DESCONOCIDO if codigo == 502 else Evidencia.IMPUESTO
369
+ audita(self.estado, {'verb': self.path, 'actor': quien['actor'], 'repo': quien['repo'],
370
+ 'ok': codigo == 200, 'detail': respuesta.get('error', ''),
371
+ 'evidence': str(ev)})
372
+ return self._responde(codigo, respuesta)
373
+
374
+
375
+ class Servidor(HTTPServer):
376
+ """HTTPServer that binds without asking the network who we are.
377
+
378
+ The stdlib's `server_bind` calls `socket.getfqdn()` for a cosmetic
379
+ `server_name`, which is a reverse DNS lookup: on a machine whose resolver
380
+ is slow or unanswered it blocks for tens of seconds, and a loopback broker
381
+ that nobody can reach until DNS replies is a broker that never started.
382
+ """
383
+
384
+ def server_bind(self):
385
+ socketserver.TCPServer.server_bind(self)
386
+ self.server_name, self.server_port = self.server_address[:2]
387
+
388
+
389
+ def sirve(datos, puerto=0):
390
+ estado = estado_de(datos)
391
+ Manejador.estado = estado
392
+ servidor = Servidor(('127.0.0.1', puerto), Manejador)
393
+ url = f'http://127.0.0.1:{servidor.server_address[1]}'
394
+ _escribe_json(os.path.join(estado, 'endpoint.json'), {'url': url, 'pid': os.getpid()})
395
+ audita(estado, {'verb': 'serve', 'actor': 'owner', 'ok': True, 'detail': url,
396
+ 'evidence': str(Evidencia.IMPUESTO)})
397
+ print(url, flush=True)
398
+ try:
399
+ servidor.serve_forever()
400
+ except KeyboardInterrupt:
401
+ pass
402
+ return 0
403
+
404
+
405
+ def url_de(datos):
406
+ """The live endpoint, or '' when the broker is not running."""
407
+ punto = _lee_json(os.path.join(estado_de(datos), 'endpoint.json'), {})
408
+ try:
409
+ os.kill(int(punto.get('pid', 0)), 0)
410
+ except (OSError, ValueError):
411
+ return ''
412
+ return str(punto.get('url', ''))
413
+
414
+
415
+ def para(datos):
416
+ punto = _lee_json(os.path.join(estado_de(datos), 'endpoint.json'), {})
417
+ try:
418
+ os.kill(int(punto.get('pid', 0)), signal.SIGTERM)
419
+ return True
420
+ except (OSError, ValueError):
421
+ return False
422
+
423
+
424
+ # ── the window's side ────────────────────────────────────────────────────────
425
+
426
+
427
+ def llama(verbo, cuerpo):
428
+ """Inside the cage: read the window's own token and ask the broker."""
429
+ url = os.environ.get('CITY_BROKER_URL', '')
430
+ token = os.environ.get('CITY_BROKER_TOKEN', '')
431
+ fichero = os.environ.get('CITY_BROKER_TOKEN_FILE', '')
432
+ if not token and fichero:
433
+ with open(fichero, encoding='utf-8') as f:
434
+ token = f.read().strip()
435
+ if not url or not token:
436
+ raise ValueError('no broker configured: CITY_BROKER_URL and a token are required')
437
+ peticion = urllib.request.Request(
438
+ f'{url}/v1/{verbo}', data=json.dumps(cuerpo).encode(), method='POST',
439
+ headers={'Authorization': f'Bearer {token}', 'content-type': 'application/json'})
440
+ try:
441
+ with urllib.request.urlopen(peticion, timeout=120) as r:
442
+ return 0, r.read().decode().strip()
443
+ except urllib.error.HTTPError as e:
444
+ return 1, e.read().decode().strip()
445
+
446
+
447
+ def _analiza():
448
+ p = argparse.ArgumentParser(description='Actions with credentials the windows never hold.')
449
+ p.add_argument('orden',
450
+ choices=['serve', 'mint', 'call', 'verify', 'stop', 'url',
451
+ 'secret-set', 'secret-list'])
452
+ p.add_argument('actor_o_verbo', nargs='?', default='')
453
+ p.add_argument('--data', help='the city data dir this broker serves')
454
+ p.add_argument('--repo', help='mint: the one repo this token may act on')
455
+ p.add_argument('--file-only', action='store_true',
456
+ help='mint: print the token file path instead of the token')
457
+ p.add_argument('--port', type=int, default=0)
458
+ p.add_argument('--title', default='')
459
+ p.add_argument('--body', default='')
460
+ p.add_argument('--base', default='')
461
+ # secret-set / call fetch
462
+ p.add_argument('--value', default='', help='secret-set: the secret value')
463
+ p.add_argument('--allow-host', action='append', default=[],
464
+ help='secret-set: an exact host this secret may be sent to (repeatable)')
465
+ p.add_argument('--host', default='', help='call fetch: target host')
466
+ p.add_argument('--path', default='/', help='call fetch: absolute request path')
467
+ p.add_argument('--method', default='GET', help='call fetch: GET or POST')
468
+ p.add_argument('--secret', default='', help='call fetch: the secret name to inject')
469
+ p.add_argument('--header', default='Authorization', help='call fetch: header to carry it')
470
+ p.add_argument('--template', default='Bearer {secret}',
471
+ help='call fetch: header value, {secret} is substituted (e.g. "{secret}")')
472
+ return p.parse_args()
473
+
474
+
475
+ def _cuerpo_para(verbo, args):
476
+ if verbo == 'fetch':
477
+ cuerpo = {'host': args.host, 'path': args.path, 'method': args.method,
478
+ 'secret': args.secret, 'header': args.header, 'template': args.template}
479
+ if args.body:
480
+ cuerpo['body'] = json.loads(args.body)
481
+ return cuerpo
482
+ return {'title': args.title, 'body': args.body, 'base': args.base}
483
+
484
+
485
+ def main():
486
+ args = _analiza()
487
+ if args.orden == 'call':
488
+ codigo, salida = llama(args.actor_o_verbo, _cuerpo_para(args.actor_o_verbo, args))
489
+ print(salida)
490
+ return codigo
491
+ if not args.data:
492
+ print('--data is required', file=sys.stderr)
493
+ return 2
494
+ if args.orden == 'serve':
495
+ return sirve(args.data, args.port)
496
+ if args.orden == 'mint':
497
+ if not args.actor_o_verbo or not args.repo:
498
+ print('mint needs an actor name and --repo', file=sys.stderr)
499
+ return 2
500
+ print(acuna(args.data, args.actor_o_verbo, args.repo, solo_fichero=args.file_only))
501
+ return 0
502
+ if args.orden == 'secret-set':
503
+ if not args.actor_o_verbo:
504
+ print('secret-set needs a key name', file=sys.stderr)
505
+ return 2
506
+ hosts = guarda_secreto(args.data, args.actor_o_verbo, args.value, args.allow_host)
507
+ print(f'secret "{args.actor_o_verbo}" bound to: {", ".join(hosts)}')
508
+ return 0
509
+ if args.orden == 'secret-list':
510
+ tienda = _lee_json(_secretos_ruta(estado_de(args.data)), {})
511
+ for clave, entrada in sorted(tienda.items()):
512
+ print(f'{clave} -> {", ".join(entrada.get("hosts", []))}')
513
+ return 0
514
+ if args.orden == 'verify':
515
+ rota = verifica(estado_de(args.data))
516
+ print('audit chain intact' if not rota else f'audit chain BROKEN at line {rota}')
517
+ return 0 if not rota else 1
518
+ if args.orden == 'url':
519
+ url = url_de(args.data)
520
+ print(url)
521
+ return 0 if url else 1
522
+ print('stopped' if para(args.data) else 'no broker was running')
523
+ return 0
524
+
525
+
526
+ if __name__ == '__main__':
527
+ try:
528
+ sys.exit(main())
529
+ except (OSError, ValueError) as e:
530
+ print(f'{e}', file=sys.stderr)
531
+ sys.exit(1)