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
package/demo/units.yml ADDED
@@ -0,0 +1,33 @@
1
+ # Business units — the districts of your city.
2
+ #
3
+ # This is yours to define at install time. The map reads it, so the districts,
4
+ # their names, their colours and their order come from here and not from the
5
+ # code. The demo ships a game studio; replace it with your own units and the map
6
+ # redraws itself.
7
+ #
8
+ # id what you write in `unidad` inside parcels.yml
9
+ # name what shows on the banner
10
+ # color hex, without the #
11
+ # order left to right; the shared district usually goes last
12
+ units:
13
+ - id: mobile ; name: Mobile Games ; color: 3fb8a0 ; order: 1
14
+ - id: console ; name: Console Games ; color: 8f7ae6 ; order: 2
15
+ - id: arcade ; name: Arcade Cabinets ; color: e08a3c ; order: 3
16
+ - id: school ; name: School Editions ; color: 4a9ede ; order: 4
17
+ - id: platform ; name: Platform ; color: 6f8fbf ; order: 5
18
+ note: "store, launcher, live ops — every unit ships through it"
19
+ - id: data ; name: Player Data ; color: e2604f ; order: 6
20
+ note: "telemetry, experiments, the numbers everyone argues with"
21
+
22
+ # Two districts every city has, whatever the business.
23
+ #
24
+ # The lab does not ship yet, and it is needed. It gets its own district instead
25
+ # of falling into the shared bucket on purpose: a lab repo among the orphans
26
+ # reads as waste, and it is the opposite. Its houses are drawn with permanent
27
+ # scaffolding, and its growth is not compared against the shipping units — a lab
28
+ # house with few floors is not behind, it is early.
29
+ special:
30
+ - id: lab ; name: Lab ; color: 8fb8c9 ; order: 90
31
+ note: "doesn't ship yet, and it's needed"
32
+ - id: none ; name: No unit ; color: c8b48a ; order: 99
33
+ note: "serves several units, or none"
@@ -0,0 +1,106 @@
1
+ # Agents come first
2
+
3
+ The original model was "a repo is an agent": every window was a git checkout,
4
+ and anyone whose work was a folder of documents — no git at all — had nowhere
5
+ to live. The model is now inverted. The primary unit is the **agent**; a repo
6
+ is just one thing an agent can mount.
7
+
8
+ The inversion is total but loses nothing, because the old model is a special
9
+ case of the new one — so **existing cities keep working untouched**.
10
+
11
+ ## The model
12
+
13
+ An agent has a **workspace folder** (`<city>/agents/<slug>/`) as its working
14
+ directory, and inside it a `mounts/` folder of **symlinks** to wherever the
15
+ real work lives: a git repo, a linked worktree, or a plain folder of documents.
16
+
17
+ ```
18
+ <city>/agents/writer/
19
+ ├── mounts/
20
+ │ ├── handbook -> ~/Documents/handbook # a document folder, no git
21
+ │ └── spec -> ~/code/product/spec # a subtree of a repo
22
+ └── (the agent's own notes live directly here)
23
+ ```
24
+
25
+ An agent also has a **kind** — `code`, `knowledge`, or `coordinator` — which is
26
+ what makes the map polymorphic instead of assuming everyone ships pull requests.
27
+
28
+ ## Two card shapes, one internal model
29
+
30
+ `plugin/scripts/workspace.py` normalises **both** card shapes into one list of
31
+ `Agente`, so the launcher, the cage and the map read one model regardless of how
32
+ the card was written.
33
+
34
+ **Read forever, written never.** The legacy shape below is still parsed by
35
+ every door, so a city written a year ago opens today exactly as it did. Nothing
36
+ writes it any more: the wizard and the Hall both produce the agent-first shape
37
+ through `workspace.claves_de_roster`, because two writers of one fact is how the
38
+ terminal and the web ended up able to produce different cities for the same
39
+ city. Re-running `./bin/seat --agents` over an old card upgrades it in place.
40
+
41
+ Legacy (still read — every repo is an agent whose single mount is that repo):
42
+
43
+ ```yaml
44
+ repos: [nova, store-service]
45
+ role.nova: po
46
+ ```
47
+
48
+ Agent-first (adds `kind.<agent>` and `mounts.<agent>`):
49
+
50
+ ```yaml
51
+ agents: [writer, chair]
52
+ kind.writer: knowledge
53
+ mounts.writer: [~/Documents/handbook, ~/code/product/spec]
54
+ kind.chair: coordinator
55
+ runs.chair: claude
56
+ ```
57
+
58
+ ## How the cage follows a mount
59
+
60
+ The cage seals by path, so an agent's workspace is exactly what it makes
61
+ writable — plus the **resolved targets** of its mounts. Because seatbelt
62
+ resolves symlinks at the kernel, `mount_targets()` follows each symlink to its
63
+ real destination and the launcher passes those to `cage.py --mounts`. A mount
64
+ that resolves inside a sealed root (`~/.ssh`, the broker store, …) is refused,
65
+ never honoured: the security invariant holds under the new model, and
66
+ `bin/test-cage.py` proves the writable-mount / sealed-secret split against the
67
+ live kernel.
68
+
69
+ ## Managing mounts
70
+
71
+ ```bash
72
+ agents-city agents list --data <city> --card <card> # list normalised agents
73
+ agents-city agents mount --data <city> --agent writer --src ~/Documents/handbook
74
+ agents-city agents mounts --data <city> --agent writer # what is mounted
75
+ agents-city agents unmount --data <city> --agent writer --name handbook
76
+ ```
77
+
78
+ At session start the launcher runs `workspace.py sync` for each agent, which
79
+ creates the workspace and materialises every declared mount (skipping a missing
80
+ source with a warning rather than aborting the city).
81
+
82
+ ## Growth without git — `crecimiento.py`
83
+
84
+ A house grows by what its agent actually does, not by pull requests alone:
85
+
86
+ - **code** — floors = merged PRs, bricks = commits (an injected git counter).
87
+ - **knowledge** — floors = documents in the workspace, bricks = recent edits.
88
+ - **coordinator** — floors = recorded decisions, bricks = notices.
89
+
90
+ Every counter returns the same `{floors, bricks, activity30, signal}` shape, so
91
+ a person with no git at all now has a house that grows as their knowledge does.
92
+
93
+ ## Avatars — `avatar.py`
94
+
95
+ Each agent gets a deterministic, self-contained SVG identicon from its name
96
+ (same name → same face), with the border tinted by kind. No network, no
97
+ library, no external asset, so it drops into the Hall under its strict CSP —
98
+ `avatar.data_uri(name, kind)` is ready for an `<img src>`.
99
+
100
+ ## Presence and "thinking"
101
+
102
+ The activity pipeline already emits on `UserPromptSubmit` (a turn begins) and
103
+ `Stop` (it ends), so "active since the last prompt without a stop" is derivable
104
+ from the existing feed — the Hall renders it. This document's generators
105
+ (avatars, growth) provide the data; wiring them into the Hall view is the
106
+ rendering layer's job.
@@ -0,0 +1,55 @@
1
+ # Glossary
2
+
3
+ The code keeps a small Spanish domain vocabulary. These are the canonical v2
4
+ meanings.
5
+
6
+ | Term | Meaning |
7
+ |---|---|
8
+ | city / `ciudad` | One autonomous work domain with one owner seat |
9
+ | owner / `usuario` | The local person who may own several cities |
10
+ | seat / `asiento` | The city's only road-facing agent; owns its role and goal |
11
+ | chair / `presidente` | The seat while selecting agents, controlling turns and integrating a decision |
12
+ | committee / `comité` | A bounded evidence process among the chair and selected repo agents |
13
+ | floor / `palabra` | One evidence-backed reply requested by a member and granted or denied by the chair |
14
+ | live feed | Read-only browser view of submitted committee artifacts and protocol actions over the local WebSocket bus; never model chain-of-thought |
15
+ | act / `acta` | Durable human-readable decision, verification and closure record |
16
+ | local bus | Per-city loopback WebSocket hub carrying typed internal and road envelopes |
17
+ | native gateway | Provider-specific last mile that accepts a bus envelope through an official runtime protocol, never terminal paste |
18
+ | Claude stream gateway | Default Agents City transport: one persistent official Claude Code process receiving acknowledged JSONL turns over stdin/stdout |
19
+ | Claude Channel | Optional upstream preview path for injecting an event into an opted-in interactive session; not required by Agents City |
20
+ | terminal fallback | Explicit `terminal:<command>` compatibility mode for an unknown CLI; the only mode allowed to inject into tmux |
21
+ | address / `direccion` | Stable `owner/city` identity used on roads |
22
+ | road / `carretera` | Explicit connection from one city seat to another |
23
+ | support agent | A local agent window working inside one repo, with its own operating role; never road-facing |
24
+ | operating role | The professional perspective assigned to one repo agent; separate from chair/member authority |
25
+ | skill | A capability installed and owned by a repo/runtime, only recognised by Agents City |
26
+ | card / `ficha` | The one owner file containing chair role, repos, per-repo roles, goal and window engines |
27
+ | blank role | An explicit role with no built-in role knowledge or inferred responsibility |
28
+ | round / `ronda` | Evidence exchange by a seat across its explicit roads |
29
+ | notice / `carta` | A concrete change one city tells another city may affect its domain |
30
+ | house / `casa` | One parcel drawn on the visual map |
31
+ | parcel / `parcela` | A slice of a repo serving one unit; not necessarily a whole repo |
32
+ | district / `barrio` | The visual area belonging to one business unit |
33
+ | unit / `unidad` | A business unit represented by a district |
34
+ | worker / `obrero` | A currently active agent window drawn on a parcel |
35
+ | foreman / `perito` | Visual representation of the city seat near its support work |
36
+ | architect / `arquitecto` | Legacy map name for the goal-owning seat |
37
+ | trade / `oficio` | A role's name in the city metaphor |
38
+ | square / `plaza` | The map's live-presence area and its Durable Object |
39
+ | scaffold / `andamio` | An open pull request; visually stale after two weeks |
40
+ | floors / `pisos` | Landed work; a house grows when work lands |
41
+ | bricks / `ladrillos` | Commits not yet represented by a pull request |
42
+ | crack / `grieta` | Failing CI |
43
+ | milestone / `hito` | One day's landed work for one parcel |
44
+ | oven / `horno` | Pipeline that bakes 3D models into isometric sprites |
45
+ | step / `paso` | Distance between map plots, in tiles |
46
+
47
+ Two special visual districts remain:
48
+
49
+ - `lab`: research work that has not shipped;
50
+ - `none`: work shared by several units or assigned to none.
51
+
52
+ Some storage and API fields still use names such as `personas` for compatibility
53
+ with existing map data. They do not change the v2 ownership rule: a personal city
54
+ has exactly one owner seat; repo support agents and connected cities are separate
55
+ entities.
@@ -0,0 +1,73 @@
1
+ # The map's live layers
2
+
3
+ The map used to describe last month: floors from merged PRs, windows lit by
4
+ thirty days of activity. Three layers now describe **right now**, and all three
5
+ are derived from data the product already emits — nothing on the map is
6
+ invented at render time.
7
+
8
+ ## Presence: lights are turns
9
+
10
+ A house whose agent is mid-turn glows and breathes, with three thinking dots
11
+ over its worker (or over its roof when no worker figure is on site). A house
12
+ whose agent just stopped cools down over about two minutes. A house with no
13
+ session shows only its historical windows.
14
+
15
+ Derived entirely from the activity feed the hooks already publish:
16
+
17
+ | Event | Effect |
18
+ | --- | --- |
19
+ | `conversation.user` | the actor's house enters "in turn": pulsing glow + dots |
20
+ | `conversation.agent` | the turn ended: the glow cools instead of cutting out |
21
+ | `runtime.session.ended` | lights out, no afterglow |
22
+
23
+ Code: `city/web/src/presencia.ts`. The Hall relays these lifecycle events to
24
+ the embedded map (`isPresenceEvent` in `city/web/src/activity.ts`); they never
25
+ become speech bubbles.
26
+
27
+ ## The town hall: the committee, staged
28
+
29
+ A civic building stands past the square. When a deliberation opens it lights
30
+ up and the badge over its door tracks the state machine — the same kinds
31
+ `isSpeechEvent` already lists:
32
+
33
+ - `committee.position.submitted` — the member's position flies in from their
34
+ house **face down**: isolated positions are the committee's one hard rule,
35
+ and the map shows the rule, not just the traffic.
36
+ - `committee.position(s).revealed` — cards turn over.
37
+ - `committee.floor.requested` — a "✋ palabra?" chip hangs over the house of
38
+ whoever asked; `granted`/`denied` answer it in colour.
39
+ - `committee.verification.passed/failed` — the verifier's stamp over the door.
40
+ - `committee.closed` — the act rises from the hall, signed; the cards leave
41
+ with it. `committee.cancelled` clears the stage with no act.
42
+
43
+ One session on stage at a time; the Hall's right rail remains the transcript.
44
+ Code: `city/web/src/ayuntamiento.ts`.
45
+
46
+ ## Gates: roads made visible
47
+
48
+ One arch per road, at the city's entrance, with the road's name and its
49
+ `owner/city` address on the plate. A notice whose recipient is not in this
50
+ city flies out through the right gate and fades at the edge; one arriving from
51
+ another city comes in the same way. The map never draws the far city — a road
52
+ grants reachability, and the gate is exactly that much.
53
+
54
+ The gates come from the Hall, which is the only thing that knows the roads: on
55
+ loading the map iframe it sends a `map.config` message with them. The
56
+ standalone team map never receives one and draws no gates rather than invent
57
+ connections. Code: `city/web/src/puertas.ts`.
58
+
59
+ ## Rehearsing without a city
60
+
61
+ Every layer can be driven from the browser console on any running map:
62
+
63
+ ```js
64
+ __city.presence('conversation.user', 'nova'); // the house breathes
65
+ __city.committee('committee.opened', 'seat', 'why'); // the hall lights up
66
+ __city.committee('committee.position.submitted', 'nova');
67
+ __city.roads([{ name: 'home', address: 'you/home' }]); // gates appear
68
+ __city.letter('ada', 'you/home', 'security'); // out through the arch
69
+ ```
70
+
71
+ `prefers-reduced-motion` is honoured the way the rest of the map honours it:
72
+ every state is still shown — steady instead of breathing, placed instead of
73
+ flown.
@@ -0,0 +1,265 @@
1
+ # The cage and the broker
2
+
3
+ Yolo is non-negotiable here: a committee dies the moment every bus command
4
+ needs a human. So the security model never touches the approval axis. It
5
+ narrows the other one — what a window can *reach* — at the kernel and at the
6
+ credential store, where a language model cannot talk its way past it.
7
+
8
+ The threat this is built for is the one nobody has solved: prompt injection.
9
+ A repo window reads text that ultimately came from outside — an issue, a log,
10
+ a committee assignment quoting a road message — and no instruction to "treat
11
+ it as untrusted" is a security control. The goal is therefore not to prevent
12
+ the injection but to make a successful one worthless: nothing to steal, no
13
+ credential to hold, and a signed trail of everything it tried.
14
+
15
+ ## Layer 1 — the cage (`plugin/scripts/cage.py`)
16
+
17
+ Claude, OpenCode and Kimi agent windows launch confined by the kernel. Two
18
+ mechanisms, one meaning — and the meaning is what `cage.py` owns, so the
19
+ launcher asks for a prefix and never learns which kernel it is on:
20
+
21
+ ```
22
+ macOS sandbox-exec -f ~/.agents-city/.runtime/cage/<window>.sb <runtime …>
23
+ Linux bwrap --ro-bind / / … --tmpfs ~/.ssh … <runtime …>
24
+ ```
25
+
26
+ On macOS the profile reads top to bottom: allow everything, deny all writes,
27
+ re-allow the working set, then seal the secrets — reads and writes both. The
28
+ semantics (last matching rule wins; children and grandchildren inherit) were
29
+ verified on a real machine before a line of it was written.
30
+
31
+ On Linux the same shape is expressed as mounts, applied in the same order and
32
+ with the same last-one-wins rule: the whole filesystem read-only, the working
33
+ set re-bound writable, each sealed directory replaced by an empty tmpfs, each
34
+ sealed file replaced by `/dev/null`, and finally this window's own broker token
35
+ re-admitted read-only. A sealed path is not refused — it is *not there*.
36
+
37
+ Availability is checked by doing, not by looking: `bwrap_sirve()` builds a real
38
+ namespace once and remembers the answer, because Ubuntu 24's AppArmor policy and
39
+ hardened kernels can refuse unprivileged user namespaces even with bubblewrap
40
+ installed. Where the cage cannot run, the launcher says so and starts uncaged —
41
+ the behaviour the product always had. `bin/test-cage.py` re-proves both cages
42
+ live: seatbelt on every macOS run, bubblewrap on every Linux run.
43
+
44
+ Inside the cage a window **can**: work freely in its own repo, run builds,
45
+ reach the network, keep its runtime state (`~/.claude`, `~/.codex`,
46
+ `~/.agents-city`, build caches). It is never asked a question — yolo intact.
47
+
48
+ Inside the cage a window **cannot**: read or write `~/.ssh`, `~/.aws`,
49
+ `~/.kube`, `~/.gnupg`, `~/.docker`, `~/.config/gcloud`, `~/.config/gh`,
50
+ `~/.git-credentials`, `~/.netrc`, `~/.pgpass`, cargo credentials,
51
+ `~/.claude/.credentials.json`, any remote road `.env` under
52
+ `~/.claude/channels/`, or the broker's state; nor write anywhere outside its
53
+ repo and the allowed runtime/cache set — other repos on the machine included.
54
+
55
+ `~/.claude/.credentials.json` earns its own line because it is the one this
56
+ product created: `~/.claude` stays writable for runtime state, and outside
57
+ macOS there is no Keychain, so Claude Code writes its OAuth tokens there as
58
+ plain JSON. Every third-party credential store was sealed while ours was not,
59
+ which is the sort of hole that only a review looking for it finds.
60
+
61
+ ### Where the two cages differ, exactly
62
+
63
+ They seal the same set, and both are re-proved live on every CI run — seatbelt
64
+ on macOS, bubblewrap on Linux. One difference is real and worth knowing:
65
+
66
+ macOS states the road-token rule as a **pattern** (`channels/*/.env`), so a
67
+ road created after the window started is sealed too. Linux states it as one
68
+ **mount per file**, and a mount needs a path that exists — so a road opened
69
+ mid-session is not sealed inside windows that were already running. It is
70
+ sealed for every window started afterwards, and closing and reopening the city
71
+ closes the gap. The alternative — hiding the whole `channels` directory — would
72
+ also hide what the in-window hooks legitimately read, so this stays a known,
73
+ bounded difference rather than a silent one.
74
+
75
+ Dials, all environment variables read at launch:
76
+
77
+ | Variable | Effect |
78
+ | --- | --- |
79
+ | `CITY_CAGE=0` | launch every window uncaged — exactly the old behaviour |
80
+ | `CITY_CAGE_DENY=a:b` | extra paths to seal (e.g. `~/.npmrc`, see below) |
81
+ | `CITY_CAGE_ALLOW_WRITE=a:b` | extra writable roots for unusual toolchains |
82
+
83
+ On Linux or any machine without `sandbox-exec`, the prefix is empty and
84
+ nothing changes. The seat is deliberately never caged: it is the permissioned,
85
+ non-yolo side that holds the road token and asks the owner.
86
+
87
+ Codex uses exactly one confinement layer: its native `workspace-write` sandbox
88
+ (network enabled only in auto mode). It is deliberately not wrapped in the
89
+ outer seatbelt profile because Codex MCPs such as `node_repl` create their own
90
+ sandboxed worker; macOS rejects that second `sandbox_apply` when the entire
91
+ gateway already runs under seatbelt. `CITY_CAGE=0` deliberately changes Codex
92
+ to `dangerFullAccess` and launches the other providers without their outer
93
+ cage.
94
+
95
+ At startup, the Codex gateway also reads the effective MCP registry through the
96
+ Codex CLI. An enabled stdio MCP whose executable is provably absent is disabled
97
+ only through a process-local `-c mcp_servers.<name>.enabled=false` override for
98
+ app-server and its TUI. The owner's `~/.codex/config.toml` is never rewritten;
99
+ healthy, URL-based and uncertain integrations are left alone. Each scoped
100
+ disable is recorded as `codex.mcp.unavailable.disabled` without MCP environment
101
+ values.
102
+
103
+ ## Layer 2 — the credential broker (`plugin/scripts/broker.py`)
104
+
105
+ A caged window cannot read the `gh` token — that is the cage working. But
106
+ opening a pull request is legitimate work, so the window asks a small process
107
+ the owner runs outside every cage, and that process acts with the real
108
+ credentials after validating the request:
109
+
110
+ ```bash
111
+ CITY_BROKER=1 agents-city seat # start the session with a broker
112
+ python3 plugin/scripts/broker.py call pr --title "Add X" --body "why"
113
+ python3 plugin/scripts/broker.py call push
114
+ ```
115
+
116
+ - One token per window, minted at session start, **bound to one repo**: a
117
+ stolen token cannot choose a different target.
118
+ - Only the SHA-256 of a token is stored; the token itself travels as a file
119
+ path whose read is the single exception the window's cage re-allows.
120
+ - The broker refuses any action on the default branch, always. Pushes are
121
+ `git push origin HEAD` — no force, no refspec.
122
+ - The broker is opt-in (`CITY_BROKER=1`) while it covers only `pr` and `push`;
123
+ stop it with `broker.py stop --data <city-dir>`.
124
+
125
+ ## Layer 3 — the audit chain, with an evidence vocabulary
126
+
127
+ Every broker request — served or refused — appends one line to
128
+ `~/.agents-city/.runtime/broker/<id>/audit.log`, and each line carries the
129
+ SHA-256 of the previous one. The cage denies repo windows the whole directory,
130
+ so an agent cannot rewrite its own history, and `broker.py verify` catches a
131
+ single flipped byte. `bin/test-broker.py` proves both directions.
132
+
133
+ A hash chain proves a line was not altered; it says nothing about what the line
134
+ proves. So every line also carries an evidence state (`plugin/scripts/evidencia.py`):
135
+ `enforced` (a control decided the outcome), `attribution-only`, `unattributed`,
136
+ `unknown`, `ambiguous`, `unsupported`. Two rules hold wherever it is read —
137
+ `unknown` never means allowed, and `ambiguous` is never resolved by picking a
138
+ candidate. A missing binding is `unknown`, which authorises nothing; only
139
+ `enforced` is proof a control fired.
140
+
141
+ ## Layer 4 — path containment, resolved twice
142
+
143
+ The cage's write-allow set and the broker's repo binding both trust paths, and
144
+ `plugin/scripts/rutas.py` answers the same question for both: it resolves a path
145
+ through the deepest ancestor that exists — so a missing leaf under a symlinked
146
+ parent still lands on the real target — and refuses a path both when it sits
147
+ inside a sealed root and when it *covers* one. The cage's own guarantee is the
148
+ ordering (the secret-deny block is emitted last and wins), so a covering
149
+ writable root is safe; what containment adds is refusing a repo rooted on the
150
+ credential store and dropping an owner `CITY_CAGE_ALLOW_WRITE` that lands inside
151
+ a seal. `bin/test-rutas.py` drives the primitive directly.
152
+
153
+ ## Layer 5 — road admission as a gate graph
154
+
155
+ An inbound road message is admitted by an ordered gate graph
156
+ (`plugin/scripts/admision.py`), not a boolean: address → road → sender →
157
+ payload, first blocker decisive. It returns a stable reason code
158
+ (`road_missing`, `sender_not_paired`, …) and, per gate, a redacted diagnostic —
159
+ counts and opaque ids, never a raw allowlist entry — so a refusal is
160
+ explainable without leaking who is allowed. A road that matches but whose sender
161
+ does not is an explicit block; an unknown sender under pairing yields
162
+ `pairing-required` rather than a silent drop.
163
+
164
+ ## Layer 6 — road pairing
165
+
166
+ An unknown city earns the right to write to a seat through a short code
167
+ (`plugin/scripts/pairing.py`): eight characters from an unambiguous alphabet, a
168
+ one-hour TTL, at most three pending per city, and an opaque approval id so
169
+ tooling never echoes the human code. The code is revealed once per sender per
170
+ window, so re-messaging cannot re-spam it. Approving grants exactly one thing —
171
+ permission for that address to write to this seat — never road membership, never
172
+ chair authority. The store is consulted only under the pairing policy; it can
173
+ never widen an existing allowlist.
174
+
175
+ ## Layer 7 — untrusted road text
176
+
177
+ Text arriving over a road is wrapped once, on arrival, in an unforgeable
178
+ boundary (`plugin/channel/untrusted.ts`): the opening marker carries a fresh
179
+ random id per wrap, so a message that pastes its own closing marker cannot guess
180
+ the id and cannot smuggle the rest as trusted text. Chat-template role tokens
181
+ (`<|im_start|>`, `[INST]`, `<start_of_turn>`, …) are defanged first, so text
182
+ cannot forge a synthetic system or assistant turn on a self-hosted backend. It
183
+ is defence in depth for the seat, not a promise the model obeys the boundary.
184
+
185
+ ## Host-bound secrets — the broker without handing over the key
186
+
187
+ A caged window cannot read an API token, and the broker does not hand it one.
188
+ The owner binds a secret to an exact host set (`broker.py secret-set OPENAI
189
+ --value … --allow-host api.openai.com`; no wildcards, no ports), and a window
190
+ asks the broker to make the call carrying the secret by *name*
191
+ (`broker.py call fetch --secret OPENAI --host api.openai.com --path /v1/…`). The
192
+ broker injects the value only when the target host matches a bound one, and
193
+ never returns it. A leaked transcript or `ps` line holds the request, never the
194
+ credential.
195
+
196
+ ## The Hall protocol — writing to the seat from a page
197
+
198
+ When the local web Hall is built, it talks to the bus that serves it over one
199
+ same-origin socket, on the contract in `plugin/scripts/hall_protocol.py`: three
200
+ frames (`req`/`res`/`event`), a per-connection monotonic sequence, and the rule
201
+ that events are never replayed — on a gap the client re-fetches a snapshot. The
202
+ method set is closed (`os.system` is not a method); writing to the seat is one
203
+ call with a queue mode (`start`/`steer`/`queue`/`note`) so an idle and a busy
204
+ seat are the same call. Loopback is a pairing convenience, never a substitute
205
+ for the seat's own admission.
206
+
207
+ ## Config migrations — `doctor`
208
+
209
+ The runtime reads one config schema. Every change that can invalidate an
210
+ existing config ships a migration in `plugin/scripts/doctor.py` that detects the
211
+ old shape, explains it in one line, writes a timestamped backup, and rewrites to
212
+ canonical form. Migrations are idempotent and ordered, so a file needing several
213
+ converges in one pass and a canonical file is left untouched. `--fix` writes;
214
+ the default is a dry-run report.
215
+
216
+ ## The ratchet
217
+
218
+ `bin/test-security.py` is not a unit suite but a firewall of cross-cutting
219
+ invariants: the secret set always contains `~/.ssh`/`~/.git-credentials`/gh, the
220
+ seal block is always emitted after the write-allow block, `unknown` evidence
221
+ never authorises, an admission decision never carries a raw allowlist entry, the
222
+ Hall method set stays closed, and the launcher never wraps the seat window in
223
+ the cage. Each past sharp edge becomes a line here, and the line stays.
224
+
225
+ ## Scope — the conscience stays inside the city
226
+
227
+ Installing the plugin does not enrol every Claude session on the machine.
228
+ Every hook sources `plugin/hooks/solo-en-ciudad.sh` first: outside a city
229
+ runtime — no `CITY_BUS_ACTOR`, the identity only `city-session.sh` sets — the
230
+ hook answers `{}` and leaves. No review passes, no digging notes, no notice
231
+ judgements, no token counters in plain conversations. The MCP server already
232
+ declared itself inactive without an actor identity; this extends the same rule
233
+ to the hooks, which were the remaining machine-wide surface.
234
+
235
+ The machine-wide conscience still exists as an explicit choice:
236
+ `CITY_HOOKS=everywhere`, in the environment or in
237
+ `~/.claude/channels/city-bus/.env`. Second-order effects are opted into, never
238
+ discovered. `bin/test-contracts.py` holds both sides: every hook is mute
239
+ outside a city, and both opt-in paths open the gate.
240
+
241
+ ## What this does NOT do, in writing
242
+
243
+ - **Prompt injection still exists.** The cage bounds what it is worth.
244
+ - **Keychain credentials travel over IPC, not file reads.** A jailed `git
245
+ push` still works when GitHub credentials live in the macOS keychain; the
246
+ broker and forge-side protected branches are the guard there. If you keep
247
+ `~/.git-credentials` in plain text, move it: `git config --global
248
+ credential.helper osxkeychain`.
249
+ - **Outbound network stays open** — that is what keeps yolo useful. A window
250
+ can still exfiltrate what it can already see: its own repo.
251
+ - **Everything runs as your OS user.** The cage narrows file reach; it is not
252
+ hostile-process isolation. Untrusted code still belongs in a separate user,
253
+ VM or container.
254
+ - `~/.npmrc` stays readable by default because denying it breaks `npm install`
255
+ for owners with a registry config. If yours holds tokens, add it to
256
+ `CITY_CAGE_DENY`.
257
+
258
+ ## Where this lands against an exposed-gateway agent
259
+
260
+ The criticisms that stuck to OpenClaw-class setups were: reachable from the
261
+ network, broad host access, credentials in reach of the model. Here: nothing
262
+ listens beyond `127.0.0.1`, writes are kernel-bounded per repo, and the
263
+ credentials a hijacked window would want are either unreadable (cage) or never
264
+ held (broker). The fourth problem — the model reads untrusted text — remains
265
+ everyone's, and the three layers above exist to make it survivable.