agents-city 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) 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 +2226 -0
  5. package/README.md +2196 -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 +119 -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/doctor +3 -0
  28. package/bin/exit +5 -0
  29. package/bin/hall +73 -0
  30. package/bin/hall.html +498 -0
  31. package/bin/logs +3 -0
  32. package/bin/navegador.mjs +243 -0
  33. package/bin/report.py +14 -0
  34. package/bin/reset +3 -0
  35. package/bin/road +3 -0
  36. package/bin/seat +10 -0
  37. package/bin/serve.py +1579 -0
  38. package/bin/setup.py +753 -0
  39. package/bin/shortcut +3 -0
  40. package/bin/skills +3 -0
  41. package/bin/test +133 -0
  42. package/bin/test-actualiza.py +130 -0
  43. package/bin/test-adapter.py +183 -0
  44. package/bin/test-admision.py +99 -0
  45. package/bin/test-atajos.py +301 -0
  46. package/bin/test-avatar.py +58 -0
  47. package/bin/test-benchmark.py +40 -0
  48. package/bin/test-broker.py +334 -0
  49. package/bin/test-cage.py +374 -0
  50. package/bin/test-card.py +402 -0
  51. package/bin/test-channel.py +379 -0
  52. package/bin/test-cities.py +453 -0
  53. package/bin/test-claude-runtime.py +252 -0
  54. package/bin/test-committee.py +340 -0
  55. package/bin/test-contracts.py +551 -0
  56. package/bin/test-crecimiento.py +164 -0
  57. package/bin/test-demo.py +308 -0
  58. package/bin/test-doctor.py +139 -0
  59. package/bin/test-domains.py +151 -0
  60. package/bin/test-evidencia.py +50 -0
  61. package/bin/test-exit.py +128 -0
  62. package/bin/test-hall-protocol.py +93 -0
  63. package/bin/test-launch.py +169 -0
  64. package/bin/test-live-feed.py +508 -0
  65. package/bin/test-navegador.py +140 -0
  66. package/bin/test-pairing.py +99 -0
  67. package/bin/test-parcels.py +283 -0
  68. package/bin/test-runtime-failures.py +252 -0
  69. package/bin/test-runtime-ui.py +488 -0
  70. package/bin/test-runtime.py +268 -0
  71. package/bin/test-rutas.py +80 -0
  72. package/bin/test-seat.py +1598 -0
  73. package/bin/test-security.py +150 -0
  74. package/bin/test-serve.py +1251 -0
  75. package/bin/test-stress.py +64 -0
  76. package/bin/test-widgets.py +143 -0
  77. package/bin/test-workspace.py +251 -0
  78. package/bin/testlib.py +118 -0
  79. package/bin/tokens.py +14 -0
  80. package/bin/update +3 -0
  81. package/bus/scripts/deploy.sh +60 -0
  82. package/bus/scripts/mint-token.sh +68 -0
  83. package/bus/scripts/setup-dev.sh +79 -0
  84. package/bus/scripts/test-channel.ts +8 -0
  85. package/bus/scripts/test-hub.ts +266 -0
  86. package/bus/scripts/test-local.sh +40 -0
  87. package/bus/scripts/test-queue.ts +132 -0
  88. package/bus/worker/package-lock.json +1971 -0
  89. package/bus/worker/package.json +17 -0
  90. package/bus/worker/src/index.ts +485 -0
  91. package/bus/worker/tsconfig.json +13 -0
  92. package/bus/worker/wrangler.toml +32 -0
  93. package/city/oven/README.md +59 -0
  94. package/city/oven/collect.py +43 -0
  95. package/city/oven/oven.html +4 -0
  96. package/city/scripts/history.py +120 -0
  97. package/city/scripts/seed.py +442 -0
  98. package/city/web/assets/modelos/License.txt +28 -0
  99. package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
  100. package/city/web/assets/modelos/building-a.glb +0 -0
  101. package/city/web/assets/modelos/building-b.glb +0 -0
  102. package/city/web/assets/modelos/building-c.glb +0 -0
  103. package/city/web/assets/modelos/building-d.glb +0 -0
  104. package/city/web/assets/modelos/building-e.glb +0 -0
  105. package/city/web/assets/modelos/building-f.glb +0 -0
  106. package/city/web/assets/modelos/building-g.glb +0 -0
  107. package/city/web/assets/modelos/building-h.glb +0 -0
  108. package/city/web/assets/sprites/building-a.png +0 -0
  109. package/city/web/assets/sprites/building-b.png +0 -0
  110. package/city/web/assets/sprites/building-c.png +0 -0
  111. package/city/web/assets/sprites/building-d.png +0 -0
  112. package/city/web/assets/sprites/building-e.png +0 -0
  113. package/city/web/assets/sprites/building-f.png +0 -0
  114. package/city/web/assets/sprites/building-g.png +0 -0
  115. package/city/web/assets/sprites/building-h.png +0 -0
  116. package/city/web/assets/sprites/building-i.png +0 -0
  117. package/city/web/assets/sprites/building-j.png +0 -0
  118. package/city/web/assets/sprites/building-k.png +0 -0
  119. package/city/web/assets/sprites/building-l.png +0 -0
  120. package/city/web/assets/sprites/building-m.png +0 -0
  121. package/city/web/assets/sprites/building-n.png +0 -0
  122. package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
  123. package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
  124. package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
  125. package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
  126. package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
  127. package/city/web/assets/sprites/catalogo.json +47 -0
  128. package/city/web/assets/sprites/detail-awning-wide.png +0 -0
  129. package/city/web/assets/sprites/detail-awning.png +0 -0
  130. package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
  131. package/city/web/assets/sprites/detail-overhang.png +0 -0
  132. package/city/web/assets/sprites/detail-parasol-a.png +0 -0
  133. package/city/web/assets/sprites/detail-parasol-b.png +0 -0
  134. package/city/web/assets/sprites/low-detail-building-a.png +0 -0
  135. package/city/web/assets/sprites/low-detail-building-b.png +0 -0
  136. package/city/web/assets/sprites/low-detail-building-c.png +0 -0
  137. package/city/web/assets/sprites/low-detail-building-d.png +0 -0
  138. package/city/web/assets/sprites/low-detail-building-e.png +0 -0
  139. package/city/web/assets/sprites/low-detail-building-f.png +0 -0
  140. package/city/web/assets/sprites/low-detail-building-g.png +0 -0
  141. package/city/web/assets/sprites/low-detail-building-h.png +0 -0
  142. package/city/web/assets/sprites/low-detail-building-i.png +0 -0
  143. package/city/web/assets/sprites/low-detail-building-j.png +0 -0
  144. package/city/web/assets/sprites/low-detail-building-k.png +0 -0
  145. package/city/web/assets/sprites/low-detail-building-l.png +0 -0
  146. package/city/web/assets/sprites/low-detail-building-m.png +0 -0
  147. package/city/web/assets/sprites/low-detail-building-n.png +0 -0
  148. package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
  149. package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
  150. package/city/web/assets/sprites/medidas.json +248 -0
  151. package/city/web/dist/city.js +939 -0
  152. package/city/web/dist/index.html +294 -0
  153. package/city/web/dist/modelos/License.txt +28 -0
  154. package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
  155. package/city/web/dist/modelos/building-a.glb +0 -0
  156. package/city/web/dist/modelos/building-b.glb +0 -0
  157. package/city/web/dist/modelos/building-c.glb +0 -0
  158. package/city/web/dist/modelos/building-d.glb +0 -0
  159. package/city/web/dist/modelos/building-e.glb +0 -0
  160. package/city/web/dist/modelos/building-f.glb +0 -0
  161. package/city/web/dist/modelos/building-g.glb +0 -0
  162. package/city/web/dist/modelos/building-h.glb +0 -0
  163. package/city/web/dist/sprites/building-a.png +0 -0
  164. package/city/web/dist/sprites/building-b.png +0 -0
  165. package/city/web/dist/sprites/building-c.png +0 -0
  166. package/city/web/dist/sprites/building-d.png +0 -0
  167. package/city/web/dist/sprites/building-e.png +0 -0
  168. package/city/web/dist/sprites/building-f.png +0 -0
  169. package/city/web/dist/sprites/building-g.png +0 -0
  170. package/city/web/dist/sprites/building-h.png +0 -0
  171. package/city/web/dist/sprites/building-i.png +0 -0
  172. package/city/web/dist/sprites/building-j.png +0 -0
  173. package/city/web/dist/sprites/building-k.png +0 -0
  174. package/city/web/dist/sprites/building-l.png +0 -0
  175. package/city/web/dist/sprites/building-m.png +0 -0
  176. package/city/web/dist/sprites/building-n.png +0 -0
  177. package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
  178. package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
  179. package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
  180. package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
  181. package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
  182. package/city/web/dist/sprites/catalogo.json +47 -0
  183. package/city/web/dist/sprites/detail-awning-wide.png +0 -0
  184. package/city/web/dist/sprites/detail-awning.png +0 -0
  185. package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
  186. package/city/web/dist/sprites/detail-overhang.png +0 -0
  187. package/city/web/dist/sprites/detail-parasol-a.png +0 -0
  188. package/city/web/dist/sprites/detail-parasol-b.png +0 -0
  189. package/city/web/dist/sprites/low-detail-building-a.png +0 -0
  190. package/city/web/dist/sprites/low-detail-building-b.png +0 -0
  191. package/city/web/dist/sprites/low-detail-building-c.png +0 -0
  192. package/city/web/dist/sprites/low-detail-building-d.png +0 -0
  193. package/city/web/dist/sprites/low-detail-building-e.png +0 -0
  194. package/city/web/dist/sprites/low-detail-building-f.png +0 -0
  195. package/city/web/dist/sprites/low-detail-building-g.png +0 -0
  196. package/city/web/dist/sprites/low-detail-building-h.png +0 -0
  197. package/city/web/dist/sprites/low-detail-building-i.png +0 -0
  198. package/city/web/dist/sprites/low-detail-building-j.png +0 -0
  199. package/city/web/dist/sprites/low-detail-building-k.png +0 -0
  200. package/city/web/dist/sprites/low-detail-building-l.png +0 -0
  201. package/city/web/dist/sprites/low-detail-building-m.png +0 -0
  202. package/city/web/dist/sprites/low-detail-building-n.png +0 -0
  203. package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
  204. package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
  205. package/city/web/dist/sprites/medidas.json +248 -0
  206. package/city/web/dist-hall/hall.js +1363 -0
  207. package/city/web/index.html +294 -0
  208. package/city/web/package.json +18 -0
  209. package/city/web/sella.py +21 -0
  210. package/city/web/src/activity-actors.ts +47 -0
  211. package/city/web/src/activity.ts +172 -0
  212. package/city/web/src/ayuntamiento.ts +580 -0
  213. package/city/web/src/draw.ts +640 -0
  214. package/city/web/src/game-speech.ts +79 -0
  215. package/city/web/src/hall.ts +1959 -0
  216. package/city/web/src/main.ts +2252 -0
  217. package/city/web/src/oven.ts +124 -0
  218. package/city/web/src/people.ts +244 -0
  219. package/city/web/src/presencia.ts +193 -0
  220. package/city/web/src/puertas.ts +180 -0
  221. package/city/web/src/repo-roles.ts +59 -0
  222. package/city/worker/package.json +16 -0
  223. package/city/worker/schema.sql +117 -0
  224. package/city/worker/src/index.ts +702 -0
  225. package/city/worker/src/square.ts +218 -0
  226. package/city/worker/wrangler.toml +58 -0
  227. package/demo/ada.md +50 -0
  228. package/demo/bruno.md +62 -0
  229. package/demo/camila.md +56 -0
  230. package/demo/city/ada.md +40 -0
  231. package/demo/city/city.yml +8 -0
  232. package/demo/city/parcels.yml +15 -0
  233. package/demo/city/roads.json +4 -0
  234. package/demo/city/units.yml +10 -0
  235. package/demo/clinica/city.yml +8 -0
  236. package/demo/clinica/parcels.yml +15 -0
  237. package/demo/clinica/roads.json +4 -0
  238. package/demo/clinica/units.yml +9 -0
  239. package/demo/clinica/vera.md +46 -0
  240. package/demo/dante.md +54 -0
  241. package/demo/despacho/city.yml +8 -0
  242. package/demo/despacho/marta.md +46 -0
  243. package/demo/despacho/parcels.yml +15 -0
  244. package/demo/despacho/roads.json +4 -0
  245. package/demo/despacho/units.yml +9 -0
  246. package/demo/elsa.md +50 -0
  247. package/demo/farid.md +52 -0
  248. package/demo/greta.md +50 -0
  249. package/demo/hugo.md +49 -0
  250. package/demo/iris.md +49 -0
  251. package/demo/jonas.md +51 -0
  252. package/demo/kira.md +51 -0
  253. package/demo/luca.md +49 -0
  254. package/demo/parcels.yml +92 -0
  255. package/demo/seed.py +20 -0
  256. package/demo/show.py +161 -0
  257. package/demo/stories.py +824 -0
  258. package/demo/units.yml +33 -0
  259. package/docs/agents-first.md +106 -0
  260. package/docs/glossary.md +55 -0
  261. package/docs/map-live-layers.md +73 -0
  262. package/docs/security.md +265 -0
  263. package/docs/self-host.md +151 -0
  264. package/docs/testing.md +205 -0
  265. package/package.json +73 -0
  266. package/plugin/.claude-plugin/plugin.json +62 -0
  267. package/plugin/.mcp.json +15 -0
  268. package/plugin/channel/activity-cli.ts +16 -0
  269. package/plugin/channel/adapter-prompts.ts +113 -0
  270. package/plugin/channel/adapter.js +4292 -0
  271. package/plugin/channel/adapter.ts +133 -0
  272. package/plugin/channel/bus.js +20188 -0
  273. package/plugin/channel/bus.ts +131 -0
  274. package/plugin/channel/city-config.ts +125 -0
  275. package/plugin/channel/claude-channel.ts +58 -0
  276. package/plugin/channel/cli-args.ts +75 -0
  277. package/plugin/channel/client.js +4200 -0
  278. package/plugin/channel/client.ts +38 -0
  279. package/plugin/channel/committee/activity.ts +256 -0
  280. package/plugin/channel/committee/collection.ts +154 -0
  281. package/plugin/channel/committee/decision.ts +175 -0
  282. package/plugin/channel/committee/floor.ts +135 -0
  283. package/plugin/channel/committee/guards.ts +30 -0
  284. package/plugin/channel/committee/history.ts +38 -0
  285. package/plugin/channel/committee/render.ts +102 -0
  286. package/plugin/channel/committee/service.ts +72 -0
  287. package/plugin/channel/committee/storage.ts +92 -0
  288. package/plugin/channel/committee/types.ts +163 -0
  289. package/plugin/channel/committee/view.ts +63 -0
  290. package/plugin/channel/committee-cli.ts +109 -0
  291. package/plugin/channel/delivery-metrics.ts +61 -0
  292. package/plugin/channel/delivery-queue.ts +118 -0
  293. package/plugin/channel/hub/activity-controller.ts +72 -0
  294. package/plugin/channel/hub/activity-feed.ts +169 -0
  295. package/plugin/channel/hub/committee-controller.ts +60 -0
  296. package/plugin/channel/hub/connections.ts +48 -0
  297. package/plugin/channel/hub/diagnostics.ts +67 -0
  298. package/plugin/channel/hub/envelope-validity.ts +109 -0
  299. package/plugin/channel/hub/envelopes.ts +66 -0
  300. package/plugin/channel/hub/lifecycle.ts +91 -0
  301. package/plugin/channel/hub/local-roads.ts +89 -0
  302. package/plugin/channel/hub/remote-roads.ts +165 -0
  303. package/plugin/channel/hub/road-controller.ts +99 -0
  304. package/plugin/channel/hub-client.ts +149 -0
  305. package/plugin/channel/local-hub.js +5995 -0
  306. package/plugin/channel/local-hub.ts +349 -0
  307. package/plugin/channel/map-reporter.ts +48 -0
  308. package/plugin/channel/package-lock.json +1731 -0
  309. package/plugin/channel/package.json +19 -0
  310. package/plugin/channel/protocol.ts +98 -0
  311. package/plugin/channel/road-cli.ts +18 -0
  312. package/plugin/channel/run.sh +15 -0
  313. package/plugin/channel/runtime/claude.ts +458 -0
  314. package/plugin/channel/runtime/codex-config.ts +77 -0
  315. package/plugin/channel/runtime/codex.ts +669 -0
  316. package/plugin/channel/runtime/command.ts +62 -0
  317. package/plugin/channel/runtime/factory.ts +15 -0
  318. package/plugin/channel/runtime/json-rpc.ts +154 -0
  319. package/plugin/channel/runtime/kimi.ts +277 -0
  320. package/plugin/channel/runtime/opencode.ts +248 -0
  321. package/plugin/channel/runtime/process.ts +84 -0
  322. package/plugin/channel/runtime/types.ts +45 -0
  323. package/plugin/channel/runtime-files.ts +73 -0
  324. package/plugin/channel/runtime-gateway.js +6396 -0
  325. package/plugin/channel/runtime-gateway.ts +358 -0
  326. package/plugin/channel/runtime-metrics.ts +52 -0
  327. package/plugin/channel/runtime-subscription.ts +180 -0
  328. package/plugin/channel/terminal-delivery.ts +131 -0
  329. package/plugin/channel/untrusted.ts +60 -0
  330. package/plugin/commands/committee.md +31 -0
  331. package/plugin/commands/exit.md +32 -0
  332. package/plugin/commands/goals.md +20 -0
  333. package/plugin/commands/join.md +18 -0
  334. package/plugin/commands/notice.md +20 -0
  335. package/plugin/commands/propose.md +19 -0
  336. package/plugin/commands/round.md +17 -0
  337. package/plugin/commands/session.md +22 -0
  338. package/plugin/commands/settings.md +25 -0
  339. package/plugin/commands/setup.md +21 -0
  340. package/plugin/commands/team.md +23 -0
  341. package/plugin/domains/custom.md +27 -0
  342. package/plugin/domains/finance.md +31 -0
  343. package/plugin/domains/healthcare.md +34 -0
  344. package/plugin/domains/legal.md +31 -0
  345. package/plugin/domains/marketing.md +33 -0
  346. package/plugin/domains/operations.md +31 -0
  347. package/plugin/domains/research.md +32 -0
  348. package/plugin/domains/sales.md +30 -0
  349. package/plugin/domains/software.md +37 -0
  350. package/plugin/hooks/activity.sh +6 -0
  351. package/plugin/hooks/digging.sh +68 -0
  352. package/plugin/hooks/growth.sh +42 -0
  353. package/plugin/hooks/hooks.json +106 -0
  354. package/plugin/hooks/notice-on-pr.sh +35 -0
  355. package/plugin/hooks/notice-on-stop.sh +80 -0
  356. package/plugin/hooks/notice-pending.sh +58 -0
  357. package/plugin/hooks/solo-en-ciudad.sh +23 -0
  358. package/plugin/hooks/tokens.sh +47 -0
  359. package/plugin/roles/examples/account-executive.md +25 -0
  360. package/plugin/roles/examples/ai-manager.md +34 -0
  361. package/plugin/roles/examples/associate.md +24 -0
  362. package/plugin/roles/examples/brand-lead.md +20 -0
  363. package/plugin/roles/examples/cfo.md +20 -0
  364. package/plugin/roles/examples/city-lead.md +25 -0
  365. package/plugin/roles/examples/clinical-director.md +26 -0
  366. package/plugin/roles/examples/clinical-ops.md +25 -0
  367. package/plugin/roles/examples/clinician.md +25 -0
  368. package/plugin/roles/examples/compliance.md +27 -0
  369. package/plugin/roles/examples/content.md +24 -0
  370. package/plugin/roles/examples/controller.md +27 -0
  371. package/plugin/roles/examples/cpto.md +41 -0
  372. package/plugin/roles/examples/customer-success.md +25 -0
  373. package/plugin/roles/examples/data-engineer.md +37 -0
  374. package/plugin/roles/examples/data.md +60 -0
  375. package/plugin/roles/examples/dev.md +37 -0
  376. package/plugin/roles/examples/devops.md +39 -0
  377. package/plugin/roles/examples/enablement.md +24 -0
  378. package/plugin/roles/examples/ethics.md +24 -0
  379. package/plugin/roles/examples/fin-analytics.md +27 -0
  380. package/plugin/roles/examples/health-compliance.md +25 -0
  381. package/plugin/roles/examples/health-data.md +26 -0
  382. package/plugin/roles/examples/knowledge.md +25 -0
  383. package/plugin/roles/examples/lifecycle.md +26 -0
  384. package/plugin/roles/examples/llm-engineer.md +34 -0
  385. package/plugin/roles/examples/managing-partner.md +20 -0
  386. package/plugin/roles/examples/methods.md +23 -0
  387. package/plugin/roles/examples/operations-lead.md +23 -0
  388. package/plugin/roles/examples/ops.md +24 -0
  389. package/plugin/roles/examples/patient-safety.md +26 -0
  390. package/plugin/roles/examples/performance.md +27 -0
  391. package/plugin/roles/examples/po.md +36 -0
  392. package/plugin/roles/examples/process-owner.md +23 -0
  393. package/plugin/roles/examples/product-design.md +39 -0
  394. package/plugin/roles/examples/program-manager.md +24 -0
  395. package/plugin/roles/examples/quality.md +25 -0
  396. package/plugin/roles/examples/research-director.md +24 -0
  397. package/plugin/roles/examples/research-ops.md +25 -0
  398. package/plugin/roles/examples/researcher.md +25 -0
  399. package/plugin/roles/examples/revenue-lead.md +25 -0
  400. package/plugin/roles/examples/revops.md +26 -0
  401. package/plugin/roles/examples/seo.md +27 -0
  402. package/plugin/roles/examples/specialist.md +24 -0
  403. package/plugin/scripts/actualiza.py +198 -0
  404. package/plugin/scripts/admision.py +165 -0
  405. package/plugin/scripts/apaga.py +232 -0
  406. package/plugin/scripts/atajos.py +506 -0
  407. package/plugin/scripts/avatar.py +211 -0
  408. package/plugin/scripts/broker.py +531 -0
  409. package/plugin/scripts/cage.py +492 -0
  410. package/plugin/scripts/capabilities.py +210 -0
  411. package/plugin/scripts/card.py +322 -0
  412. package/plugin/scripts/cities.py +520 -0
  413. package/plugin/scripts/city-env.sh +90 -0
  414. package/plugin/scripts/city-runtime.sh +87 -0
  415. package/plugin/scripts/city-session.sh +552 -0
  416. package/plugin/scripts/city_env.py +67 -0
  417. package/plugin/scripts/crecimiento.py +140 -0
  418. package/plugin/scripts/deliberations.py +48 -0
  419. package/plugin/scripts/doctor.py +294 -0
  420. package/plugin/scripts/domains.py +201 -0
  421. package/plugin/scripts/evidencia.py +64 -0
  422. package/plugin/scripts/find-repos.sh +111 -0
  423. package/plugin/scripts/gh.py +159 -0
  424. package/plugin/scripts/hall_protocol.py +147 -0
  425. package/plugin/scripts/hook_activity.py +139 -0
  426. package/plugin/scripts/launch.py +94 -0
  427. package/plugin/scripts/logs.py +98 -0
  428. package/plugin/scripts/pairing.py +185 -0
  429. package/plugin/scripts/parcels.py +132 -0
  430. package/plugin/scripts/read-card.py +55 -0
  431. package/plugin/scripts/report.py +201 -0
  432. package/plugin/scripts/reset.py +180 -0
  433. package/plugin/scripts/roads.py +230 -0
  434. package/plugin/scripts/roles.py +274 -0
  435. package/plugin/scripts/runtime_log.py +69 -0
  436. package/plugin/scripts/runtime_processes.py +132 -0
  437. package/plugin/scripts/rutas.py +83 -0
  438. package/plugin/scripts/seat.py +1696 -0
  439. package/plugin/scripts/tokens.py +194 -0
  440. package/plugin/scripts/trust-repos.py +59 -0
  441. package/plugin/scripts/ui.py +280 -0
  442. package/plugin/scripts/units.py +80 -0
  443. package/plugin/scripts/workspace.py +561 -0
  444. package/plugin/skills/city/SKILL.md +191 -0
  445. package/templates/blank.md +39 -0
  446. package/templates/finance.md +44 -0
  447. package/templates/legal.md +45 -0
  448. package/templates/marketing.md +48 -0
  449. package/templates/product.md +45 -0
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env python3
2
+ """The cage: one kernel-enforced confinement per agent window.
3
+
4
+ macOS gets a generated seatbelt profile; Linux gets a bubblewrap mount
5
+ namespace. Two mechanisms, one meaning — and the meaning is what this module
6
+ owns, so the launcher asks for a prefix and never learns which kernel it is on.
7
+
8
+ The yolo flag stays — a committee cannot work if every bus command needs a
9
+ human. What changes is what the kernel lets the window touch. An agent inside
10
+ the cage is never asked anything: writes land only in its own repo and the
11
+ runtime state it legitimately owns, and the files that turn a prompt injection
12
+ into a credential theft (`~/.ssh`, `~/.git-credentials`, cloud configs, the
13
+ `gh` token) simply do not exist for it.
14
+
15
+ Seatbelt semantics, verified on a real machine before this was written:
16
+ the LAST matching rule wins, children and grandchildren inherit the profile,
17
+ and the network stays open unless denied. So the profile reads top to bottom
18
+ as: allow everything, forbid all writes, re-allow the working set, and finally
19
+ seal the secrets — reads and writes both.
20
+
21
+ Honest limits, documented rather than hidden: credentials held in the macOS
22
+ keychain travel over IPC, not file reads, so a jailed `git push` still works
23
+ when the owner keeps GitHub credentials there; outbound network stays open;
24
+ and every window still runs as the same OS user. The cage narrows what a
25
+ compromised window can steal — it is not hostile-process isolation.
26
+ """
27
+
28
+ import argparse
29
+ import functools
30
+ import os
31
+ import re
32
+ import shlex
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+
37
+ import rutas
38
+
39
+ # Read+write denied inside the cage: directories that exist to hold credentials.
40
+ SECRETOS_DIR = (
41
+ '.ssh', '.aws', '.kube', '.gnupg',
42
+ os.path.join('.config', 'gcloud'),
43
+ os.path.join('.config', 'gh'),
44
+ os.path.join('.docker'),
45
+ )
46
+ # Single files with the same job. `~/.npmrc` is deliberately NOT here: denying
47
+ # it breaks every `npm install` for owners who configure a registry, and a
48
+ # broken product protects nobody. Owners who keep tokens there add it through
49
+ # CITY_CAGE_DENY.
50
+ SECRETOS_FICHERO = (
51
+ '.git-credentials', '.netrc', '.pgpass',
52
+ # The one this product created itself. Outside macOS there is no Keychain
53
+ # and Claude Code writes its OAuth access and refresh tokens here as plain
54
+ # JSON — inside a directory the cage keeps WRITABLE for runtime state. Every
55
+ # third-party credential store was sealed and ours was not, which made the
56
+ # cage's promise false exactly where it mattered most.
57
+ os.path.join('.claude', '.credentials.json'),
58
+ )
59
+
60
+ # Writable beyond the repo: the runtime state and build caches a working agent
61
+ # legitimately owns. Everything here is cache or agent state, never a secret —
62
+ # with the exceptions sealed again further down.
63
+ ESCRITURA_CASA = (
64
+ '.claude', '.agents-city', '.codex', '.opencode', '.kimi',
65
+ '.npm', '.cache', '.cargo', '.rustup', '.gradle', '.m2',
66
+ '.pnpm-store', '.bun',
67
+ )
68
+ #: Where each kernel keeps the rest of that state. Splitting the system paths
69
+ #: and leaving these shared was an oversight with teeth: `Library/pnpm` is
70
+ #: pnpm's macOS home and its Linux twin `~/.local/share/pnpm` was missing, so a
71
+ #: caged `pnpm add -g`, `pip install --user`, `pipx` or `go install` all failed
72
+ #: with a read-only filesystem for reasons nobody would connect to the cage.
73
+ ESCRITURA_CASA_MAC = (
74
+ os.path.join('Library', 'Caches'),
75
+ os.path.join('Library', 'Logs'),
76
+ os.path.join('Library', 'pnpm'),
77
+ os.path.join('Library', 'Developer'),
78
+ )
79
+ ESCRITURA_CASA_LINUX = (
80
+ os.path.join('.local', 'share'),
81
+ os.path.join('.local', 'state'),
82
+ os.path.join('.local', 'bin'),
83
+ '.config',
84
+ 'go',
85
+ )
86
+ #: Temporary and device paths a working agent legitimately writes, per kernel.
87
+ #: Keyed by platform because "where /tmp really is" is the one fact the two
88
+ #: cages genuinely disagree on — everything else below is shared.
89
+ ESCRITURA_SISTEMA = ('/dev', '/private/tmp', '/private/var/tmp', '/private/var/folders')
90
+ #: `/dev` is deliberately absent: the argv already gives the sandbox its own
91
+ #: device tree, and re-binding the host's over it would undo that.
92
+ ESCRITURA_SISTEMA_LINUX = ('/tmp', '/var/tmp', '/run/user')
93
+
94
+
95
+ def escritura_sistema():
96
+ """The temp and device paths of the kernel we are RUNNING on.
97
+
98
+ Only for callers that have no cage of their own to name. Each renderer
99
+ passes its own tuple instead: the bubblewrap argv is the Linux cage
100
+ wherever it is built, so a macOS machine writing one — a test, a review —
101
+ must still produce Linux paths, or the thing under test is not the thing
102
+ that ships.
103
+ """
104
+ return ESCRITURA_SISTEMA_LINUX if sys.platform.startswith('linux') else ESCRITURA_SISTEMA
105
+
106
+
107
+ def _sbpl(ruta):
108
+ """Quote one path for SBPL: backslashes and double quotes escaped."""
109
+ return '"' + ruta.replace('\\', '\\\\').replace('"', '\\"') + '"'
110
+
111
+
112
+ def agents_home(casa):
113
+ """The Agents City state root, honouring AGENTS_CITY_HOME like the broker.
114
+
115
+ The broker writes its token store under this root (`broker.estado_de`), so
116
+ the cage must seal the *same* root it uses — not a hardcoded `~/.agents-city`
117
+ that silently misses the broker dir when the owner relocated the home.
118
+ """
119
+ hogar = os.environ.get('AGENTS_CITY_HOME') or os.path.join(casa, '.agents-city')
120
+ return rutas.canonicaliza(hogar)
121
+
122
+
123
+ def sellados_por_tipo(casa):
124
+ """The sealed roots, each with what it IS: a file or a directory.
125
+
126
+ Decided by DECLARATION first and by the disk second. It matters because the
127
+ two cages render the two kinds differently — a directory becomes an empty
128
+ tmpfs, a file becomes /dev/null — and a credential file that does not exist
129
+ yet must still be sealed as a file, or the cage mounts a directory in its
130
+ place and the runtime can never create it.
131
+ """
132
+ ac = agents_home(casa)
133
+ fuera = {}
134
+ for d in SECRETOS_DIR:
135
+ fuera[rutas.canonicaliza(os.path.join(casa, d))] = 'dir'
136
+ for f in SECRETOS_FICHERO:
137
+ fuera[rutas.canonicaliza(os.path.join(casa, f))] = 'file'
138
+ fuera[rutas.canonicaliza(os.path.join(ac, '.runtime', 'broker'))] = 'dir'
139
+ for f in ('credentials', 'credentials.toml'):
140
+ fuera[rutas.canonicaliza(os.path.join(casa, '.cargo', f))] = 'file'
141
+ # An owner's own denies: nothing declares their kind, so ask the disk and
142
+ # treat anything else as a directory, which seals more rather than less.
143
+ for p in os.environ.get('CITY_CAGE_DENY', '').split(':'):
144
+ if not p:
145
+ continue
146
+ real = rutas.canonicaliza(os.path.expanduser(p))
147
+ fuera[real] = 'file' if os.path.isfile(real) else 'dir'
148
+ return fuera
149
+
150
+
151
+ def sellados(casa):
152
+ """The canonical set of read+write-sealed roots.
153
+
154
+ The single source of truth for "must never be reachable": the deny lines
155
+ emit it, and the write-allow filter refuses any owner path that re-enters
156
+ it. Extra owner denies (CITY_CAGE_DENY) join it, so an owner can widen the
157
+ seal but the allow-list can never outrun it.
158
+ """
159
+ return list(sellados_por_tipo(casa))
160
+
161
+
162
+ def _permitidas_escritura(repo, casa, bloqueados, extra_escritura=(), sistema=None):
163
+ """Canonical write-allow roots, given the already-computed sealed set.
164
+
165
+ Covering roots are intentionally kept: the sealed-secret block is emitted
166
+ last and, under seatbelt's last-match-wins rule, re-denies any secret that
167
+ a broad writable ancestor would otherwise expose. So the ordering — not a
168
+ filter here — is the guarantee, and `bin/test-cage.py` proves it against the
169
+ live kernel. What this pass does add is a loud drop of an owner extra that
170
+ resolves strictly *inside* a sealed root: that write is already dead under
171
+ the final deny, so honouring it silently would only mislead. The relocated
172
+ state home is added explicitly so a custom AGENTS_CITY_HOME stays writable.
173
+
174
+ `extra_escritura` is where the agent-first model plugs in: the resolved
175
+ targets of the agent's workspace mounts (symlinks already followed to their
176
+ real path). They pass through the same sealed-root check as any owner extra.
177
+ """
178
+ extra = [os.path.expanduser(p)
179
+ for p in os.environ.get('CITY_CAGE_ALLOW_WRITE', '').split(':') if p]
180
+ extra += list(extra_escritura)
181
+ linux = sistema is ESCRITURA_SISTEMA_LINUX or (
182
+ sistema is None and sys.platform.startswith('linux'))
183
+ fijas = [repo, agents_home(casa), *(sistema if sistema is not None else escritura_sistema())]
184
+ fijas += [os.path.join(casa, p) for p in ESCRITURA_CASA]
185
+ fijas += [os.path.join(casa, p)
186
+ for p in (ESCRITURA_CASA_LINUX if linux else ESCRITURA_CASA_MAC)]
187
+ aceptadas = [rutas.canonicaliza(c) for c in fijas]
188
+ for c in extra:
189
+ if any(rutas.dentro_de(c, raiz) for raiz in bloqueados):
190
+ sys.stderr.write(f'cage: ignoring write-allow {c}: inside a sealed root\n')
191
+ continue
192
+ aceptadas.append(rutas.canonicaliza(c))
193
+ return aceptadas
194
+
195
+
196
+ def _lineas_permite_escritura(repo, casa, bloqueados, extra_escritura=()):
197
+ permitidas = _permitidas_escritura(repo, casa, bloqueados, extra_escritura,
198
+ sistema=ESCRITURA_SISTEMA)
199
+ lineas = [f' (subpath {_sbpl(r)})' for r in permitidas]
200
+ # Claude keeps `~/.claude.json` (and its backups) at the HOME root.
201
+ lineas.append(f' (regex #"^{re.escape(os.path.join(casa, ".claude.json"))}")')
202
+ return lineas
203
+
204
+
205
+ def carreteras_raiz(casa):
206
+ """Where a seat's remote-road channels live. One spelling, two readers."""
207
+ return os.path.join(casa, '.claude', 'channels')
208
+
209
+
210
+ def env_de_carreteras(casa):
211
+ """The remote-road token files: `~/.claude/channels/<seat>/.env`.
212
+
213
+ One definition of "these are secrets too", because the two cages render it
214
+ differently — macOS as a regex over a path that may not exist yet, Linux as
215
+ one mount per file that does. A rule written twice is a rule that will be
216
+ sealed on one kernel and open on the other.
217
+ """
218
+ canales = carreteras_raiz(casa)
219
+ try:
220
+ entradas = sorted(os.listdir(canales))
221
+ except OSError:
222
+ return []
223
+ fuera = []
224
+ for entrada in entradas:
225
+ env = os.path.join(canales, entrada, '.env')
226
+ if os.path.isfile(env):
227
+ fuera.append(env)
228
+ return fuera
229
+
230
+
231
+ def _lineas_secretos(casa, bloqueados):
232
+ tipos = sellados_por_tipo(casa)
233
+ lineas = []
234
+ for raiz in bloqueados:
235
+ # A file target uses `literal`; a directory uses `subpath` so everything
236
+ # beneath it is sealed too. The kind comes from the declaration, so a
237
+ # credential file that does not exist yet is still sealed as a file.
238
+ if tipos.get(raiz, 'dir') == 'file':
239
+ lineas.append(f' (literal {_sbpl(raiz)})')
240
+ else:
241
+ lineas.append(f' (subpath {_sbpl(raiz)})')
242
+ # Remote road tokens configured for the seat: repo windows must never read
243
+ # them, even though the rest of `~/.claude` stays open for the runtime.
244
+ canal = re.escape(carreteras_raiz(casa))
245
+ lineas.append(f' (regex #"^{canal}/[^/]+/\\.env$")')
246
+ return lineas
247
+
248
+
249
+ def perfil(repo, casa=None, fichero_token=None, extra_escritura=()):
250
+ """The SBPL profile for one window, as text. Pure: no filesystem writes.
251
+
252
+ `repo` is the window's working directory (a repo, or an agent workspace).
253
+ `extra_escritura` are additional writable roots — the resolved targets of an
254
+ agent's workspace mounts — sealed-root-checked like any other write-allow.
255
+ """
256
+ casa = rutas.canonicaliza(casa or '~')
257
+ repo = rutas.canonicaliza(repo)
258
+ if not os.path.isdir(repo):
259
+ raise ValueError(f'the cage needs an existing working directory, got: {repo}')
260
+ # Compute the sealed set once and thread it through every helper below.
261
+ bloqueados = sellados(casa)
262
+ motivo = rutas.motivo_bloqueo(repo, bloqueados)
263
+ if motivo:
264
+ raise ValueError(f'the working directory is unsafe to cage: {motivo}')
265
+ partes = [
266
+ '(version 1)',
267
+ '; Agents City cage — generated, one per window. Do not edit.',
268
+ '(allow default)',
269
+ '(deny file-write*)',
270
+ '(allow file-write*',
271
+ *_lineas_permite_escritura(repo, casa, bloqueados, extra_escritura),
272
+ ')',
273
+ '(deny file-read* file-write*',
274
+ *_lineas_secretos(casa, bloqueados),
275
+ ')',
276
+ ]
277
+ if fichero_token:
278
+ # This window's own broker token, and only this window's: the broker
279
+ # directory deny above stays in force for every other file in it.
280
+ partes.append(f'(allow file-read* (literal {_sbpl(rutas.canonicaliza(fichero_token))}))')
281
+ return '\n'.join(partes) + '\n'
282
+
283
+
284
+ # ── Linux: the same seal, built out of mounts instead of a profile ───────────
285
+ #
286
+ # Bubblewrap applies its binds in order and the last one wins, so the argv obeys
287
+ # the same ordering guarantee the profile above states — and a sealed path is
288
+ # not refused here, it is simply not mounted.
289
+
290
+ #: The base every caged launch starts from — and the exact same flags the
291
+ #: availability probe runs, so "bwrap works here" cannot mean a different
292
+ #: sandbox than the one an agent gets.
293
+ BASE_BWRAP = ['bwrap', '--ro-bind', '/', '/', '--dev', '/dev']
294
+
295
+
296
+ @functools.lru_cache(maxsize=None)
297
+ def _prueba_bwrap():
298
+ """Build one real namespace and remember whether it worked."""
299
+ if not shutil.which('bwrap'):
300
+ return False
301
+ try:
302
+ hecho = subprocess.run(
303
+ # It either builds a namespace in milliseconds or it is refused;
304
+ # nothing in between, so a long timeout only buys dead launch time.
305
+ [*BASE_BWRAP, 'true'],
306
+ capture_output=True,
307
+ timeout=5,
308
+ )
309
+ return hecho.returncode == 0
310
+ except (OSError, subprocess.SubprocessError):
311
+ return False
312
+
313
+
314
+ def bwrap_sirve():
315
+ """Whether bubblewrap can actually build a namespace here — not merely
316
+ whether the binary exists.
317
+
318
+ Ubuntu 24 restricts unprivileged user namespaces through AppArmor, and a
319
+ hardened kernel can refuse them outright. A prefix that fails at launch
320
+ would take the agent's window down with it, so this asks bwrap to do the
321
+ real thing rather than trusting `which`.
322
+
323
+ The launcher runs this module as a fresh process PER WINDOW, so the answer
324
+ is also read from and published to the environment: a city of eight agents
325
+ forks one probe, not eight. `CITY_CAGE_BWRAP=1|0` is that channel, and an
326
+ owner can set it by hand to skip the probe entirely.
327
+ """
328
+ # The environment outranks the memo on purpose: it is how the launcher hands
329
+ # one probe's answer to every window, and how an owner skips probing at all.
330
+ dicho = os.environ.get('CITY_CAGE_BWRAP')
331
+ if dicho == '0':
332
+ return False
333
+ if dicho == '1':
334
+ # Trusted, but not blindly: tmux windows inherit the server's whole
335
+ # environment, so a value set on another machine (or a stale one) would
336
+ # otherwise build a prefix that exits 127 on every single window.
337
+ return shutil.which('bwrap') is not None
338
+ return _prueba_bwrap()
339
+
340
+
341
+ def argv_bwrap(repo, casa=None, fichero_token=None, extra_escritura=()):
342
+ """The bubblewrap argv that cages one window on Linux. Pure: no writes.
343
+
344
+ Returns the prefix only — the caller appends the command to run.
345
+ """
346
+ casa = rutas.canonicaliza(casa or '~')
347
+ repo = rutas.canonicaliza(repo)
348
+ if not os.path.isdir(repo):
349
+ raise ValueError(f'the cage needs an existing working directory, got: {repo}')
350
+ bloqueados = sellados(casa)
351
+ motivo = rutas.motivo_bloqueo(repo, bloqueados)
352
+ if motivo:
353
+ raise ValueError(f'the working directory is unsafe to cage: {motivo}')
354
+
355
+ # Deliberately NOT here:
356
+ # --proc needs a PID namespace to be worth anything, and a PID
357
+ # namespace would make the runtime gateway write a
358
+ # namespace-local pid into <runtime>/gateways/<actor>.pid —
359
+ # which `agents-city exit` then signals on the host, killing
360
+ # an unrelated process. /proc arrives with the read-only bind
361
+ # of / anyway.
362
+ # --die-with-parent
363
+ # the bus hub is started detached ON PURPOSE so it outlives
364
+ # the window that happened to start it; tying the namespace's
365
+ # life to one pane would take the whole city's bus down with
366
+ # that pane. macOS has no equivalent flag either.
367
+ argv = BASE_BWRAP.copy()
368
+ # The working set, re-bound writable over the read-only world. The set is
369
+ # `_permitidas_escritura`'s answer and only that: the same list the SBPL
370
+ # profile allows, so the two cages cannot drift into permitting different
371
+ # things.
372
+ for ruta in _permitidas_escritura(repo, casa, bloqueados, extra_escritura,
373
+ sistema=ESCRITURA_SISTEMA_LINUX):
374
+ # `-try` because a build cache nobody has created yet is not an error,
375
+ # and a cage that refuses to start is a cage nobody keeps switched on.
376
+ argv += ['--bind-try', ruta, ruta]
377
+ # And then the seal. A directory becomes an empty tmpfs; a file becomes
378
+ # /dev/null, which cannot be read through.
379
+ #
380
+ # ONLY what exists is sealed, and that is not a shortcut — it is the
381
+ # difference between a cage and a machine that cannot start. `--tmpfs` has
382
+ # no `-try` form and bwrap creates the mountpoint with a single mkdir: over
383
+ # a read-only `/`, sealing an absent `~/.aws` aborts the launch, and where
384
+ # the parent IS writable it does worse — it leaves a real empty directory
385
+ # on the owner's disk, so a later `cargo login` fails forever with "Is a
386
+ # directory" and nothing points back here.
387
+ #
388
+ # Skipping an absent path costs nothing: `$HOME` itself is never writable
389
+ # inside the cage, so a window cannot create the secret it was not sealed
390
+ # from. What a seal cannot cover is a path created OUTSIDE, mid-session —
391
+ # the same bounded difference the road tokens have, documented in
392
+ # docs/security.md rather than papered over.
393
+ tipos = sellados_por_tipo(casa)
394
+ for raiz in bloqueados:
395
+ if not os.path.exists(raiz):
396
+ continue
397
+ if tipos.get(raiz, 'dir') == 'file':
398
+ argv += ['--ro-bind-try', '/dev/null', raiz]
399
+ else:
400
+ argv += ['--tmpfs', raiz]
401
+ # Claude keeps `~/.claude.json` at the HOME root, and the SBPL profile
402
+ # allows writing it. Without the same line here the Linux cage silently
403
+ # breaks the runtime it is meant to protect.
404
+ conf = rutas.canonicaliza(os.path.join(casa, '.claude.json'))
405
+ argv += ['--bind-try', conf, conf]
406
+ # Remote road tokens: the same rule the profile states as a regex, applied
407
+ # here one file at a time because a mount needs a path, not a pattern.
408
+ for env in env_de_carreteras(casa):
409
+ argv += ['--ro-bind-try', '/dev/null', env]
410
+ if fichero_token:
411
+ # Last word: this window's own token, and only this one.
412
+ real = rutas.canonicaliza(fichero_token)
413
+ argv += ['--ro-bind-try', real, real]
414
+ return argv
415
+
416
+
417
+ def disponible():
418
+ """Whether this machine can cage a window at all."""
419
+ if os.environ.get('CITY_CAGE', '1') == '0':
420
+ return False
421
+ if sys.platform == 'darwin':
422
+ return shutil.which('sandbox-exec') is not None
423
+ return sys.platform.startswith('linux') and bwrap_sirve()
424
+
425
+
426
+ def escribe_perfil(repo, ventana, casa=None, fichero_token=None, extra_escritura=()):
427
+ """Write the profile under the runtime dir and return its path."""
428
+ casa_real = os.path.realpath(casa or os.path.expanduser('~'))
429
+ directorio = os.path.join(casa_real, '.agents-city', '.runtime', 'cage')
430
+ os.makedirs(directorio, mode=0o700, exist_ok=True)
431
+ segmento = re.sub(r'[^a-zA-Z0-9_-]', '-', ventana) or 'window'
432
+ ruta = os.path.join(directorio, f'{segmento}.sb')
433
+ contenido = perfil(repo, casa=casa_real, fichero_token=fichero_token,
434
+ extra_escritura=extra_escritura)
435
+ with open(ruta, 'w', encoding='utf-8') as f:
436
+ f.write(contenido)
437
+ os.chmod(ruta, 0o600)
438
+ return ruta
439
+
440
+
441
+ def linea(repo, ventana, casa=None, fichero_token=None, extra_escritura=()):
442
+ """The launch prefix for one window, ready to prepend to a shell command:
443
+ `sandbox-exec -f <profile> ` on macOS, `bwrap … ` on Linux, or ''.
444
+
445
+ Empty means "launch uncaged": an unsupported platform, no sandboxing tool,
446
+ or the owner set CITY_CAGE=0. The caller prepends the result verbatim, so
447
+ the degraded path is exactly the behaviour the product always had.
448
+ """
449
+ if not disponible():
450
+ return ''
451
+ if sys.platform == 'darwin':
452
+ ruta = escribe_perfil(repo, ventana, casa=casa, fichero_token=fichero_token,
453
+ extra_escritura=extra_escritura)
454
+ return f'sandbox-exec -f {ruta} '
455
+ argv = argv_bwrap(repo, casa=casa, fichero_token=fichero_token,
456
+ extra_escritura=extra_escritura)
457
+ return shlex.join(argv) + ' '
458
+
459
+
460
+ def main():
461
+ p = argparse.ArgumentParser(description='One seatbelt cage per window.')
462
+ p.add_argument('orden', choices=['line', 'profile', 'check'])
463
+ p.add_argument('--repo', help='the working directory this window owns')
464
+ p.add_argument('--window', default='window', help='window name, used to name the profile')
465
+ p.add_argument('--home', help='HOME override (tests only)')
466
+ p.add_argument('--token-file', help="this window's broker token file, allowed read-only")
467
+ p.add_argument('--mounts', default='',
468
+ help='colon-separated resolved mount targets to also make writable')
469
+ args = p.parse_args()
470
+ if args.orden == 'check':
471
+ print('cage available' if disponible() else 'cage unavailable on this machine')
472
+ return 0 if disponible() else 1
473
+ if not args.repo:
474
+ print('--repo is required', file=sys.stderr)
475
+ return 2
476
+ extra = tuple(m for m in args.mounts.split(':') if m)
477
+ try:
478
+ if args.orden == 'profile':
479
+ sys.stdout.write(perfil(args.repo, casa=args.home, fichero_token=args.token_file,
480
+ extra_escritura=extra))
481
+ else:
482
+ sys.stdout.write(
483
+ linea(args.repo, args.window, casa=args.home, fichero_token=args.token_file,
484
+ extra_escritura=extra))
485
+ except (OSError, ValueError) as e:
486
+ print(f'{e}', file=sys.stderr)
487
+ return 1
488
+ return 0
489
+
490
+
491
+ if __name__ == '__main__':
492
+ sys.exit(main())