agents-city 0.3.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2021 -0
- package/README.md +1999 -0
- package/benchmarks/committee/README.md +39 -0
- package/benchmarks/committee/metrics.py +51 -0
- package/benchmarks/committee/run.py +64 -0
- package/benchmarks/committee/traces/01-single.json +8 -0
- package/benchmarks/committee/traces/02-mesh.json +16 -0
- package/benchmarks/committee/traces/03-chair.json +15 -0
- package/benchmarks/latency/README.md +35 -0
- package/benchmarks/latency/fake-claude-cli.mjs +119 -0
- package/benchmarks/latency/fake-native-server.mjs +340 -0
- package/benchmarks/latency/fake-tui.py +91 -0
- package/benchmarks/latency/live.py +380 -0
- package/benchmarks/stress/README.md +20 -0
- package/benchmarks/stress/run.py +487 -0
- package/bin/agents +4 -0
- package/bin/agents-city.js +116 -0
- package/bin/benchmark +17 -0
- package/bin/bus +6 -0
- package/bin/cities +3 -0
- package/bin/city +86 -0
- package/bin/committee +6 -0
- package/bin/demo +200 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +426 -0
- package/bin/logs +3 -0
- package/bin/report.py +14 -0
- package/bin/reset +3 -0
- package/bin/road +3 -0
- package/bin/seat +10 -0
- package/bin/serve.py +1428 -0
- package/bin/setup.py +753 -0
- package/bin/skills +3 -0
- package/bin/test +131 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-avatar.py +58 -0
- package/bin/test-benchmark.py +40 -0
- package/bin/test-broker.py +334 -0
- package/bin/test-cage.py +206 -0
- package/bin/test-card.py +402 -0
- package/bin/test-channel.py +379 -0
- package/bin/test-cities.py +453 -0
- package/bin/test-claude-runtime.py +252 -0
- package/bin/test-committee.py +340 -0
- package/bin/test-contracts.py +544 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +106 -0
- package/bin/test-domains.py +151 -0
- package/bin/test-evidencia.py +50 -0
- package/bin/test-exit.py +128 -0
- package/bin/test-hall-protocol.py +93 -0
- package/bin/test-launch.py +169 -0
- package/bin/test-live-feed.py +508 -0
- package/bin/test-pairing.py +99 -0
- package/bin/test-parcels.py +283 -0
- package/bin/test-runtime-failures.py +252 -0
- package/bin/test-runtime-ui.py +488 -0
- package/bin/test-runtime.py +268 -0
- package/bin/test-rutas.py +80 -0
- package/bin/test-seat.py +1386 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1057 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +188 -0
- package/bin/testlib.py +95 -0
- package/bin/tokens.py +14 -0
- package/bus/scripts/deploy.sh +60 -0
- package/bus/scripts/mint-token.sh +68 -0
- package/bus/scripts/setup-dev.sh +79 -0
- package/bus/scripts/test-channel.ts +8 -0
- package/bus/scripts/test-hub.ts +266 -0
- package/bus/scripts/test-local.sh +40 -0
- package/bus/scripts/test-queue.ts +132 -0
- package/bus/worker/package-lock.json +1971 -0
- package/bus/worker/package.json +17 -0
- package/bus/worker/src/index.ts +485 -0
- package/bus/worker/tsconfig.json +13 -0
- package/bus/worker/wrangler.toml +32 -0
- package/city/oven/README.md +59 -0
- package/city/oven/collect.py +43 -0
- package/city/oven/oven.html +4 -0
- package/city/scripts/history.py +120 -0
- package/city/scripts/seed.py +442 -0
- package/city/web/assets/modelos/License.txt +28 -0
- package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/assets/modelos/building-a.glb +0 -0
- package/city/web/assets/modelos/building-b.glb +0 -0
- package/city/web/assets/modelos/building-c.glb +0 -0
- package/city/web/assets/modelos/building-d.glb +0 -0
- package/city/web/assets/modelos/building-e.glb +0 -0
- package/city/web/assets/modelos/building-f.glb +0 -0
- package/city/web/assets/modelos/building-g.glb +0 -0
- package/city/web/assets/modelos/building-h.glb +0 -0
- package/city/web/assets/sprites/building-a.png +0 -0
- package/city/web/assets/sprites/building-b.png +0 -0
- package/city/web/assets/sprites/building-c.png +0 -0
- package/city/web/assets/sprites/building-d.png +0 -0
- package/city/web/assets/sprites/building-e.png +0 -0
- package/city/web/assets/sprites/building-f.png +0 -0
- package/city/web/assets/sprites/building-g.png +0 -0
- package/city/web/assets/sprites/building-h.png +0 -0
- package/city/web/assets/sprites/building-i.png +0 -0
- package/city/web/assets/sprites/building-j.png +0 -0
- package/city/web/assets/sprites/building-k.png +0 -0
- package/city/web/assets/sprites/building-l.png +0 -0
- package/city/web/assets/sprites/building-m.png +0 -0
- package/city/web/assets/sprites/building-n.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
- package/city/web/assets/sprites/catalogo.json +47 -0
- package/city/web/assets/sprites/detail-awning-wide.png +0 -0
- package/city/web/assets/sprites/detail-awning.png +0 -0
- package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
- package/city/web/assets/sprites/detail-overhang.png +0 -0
- package/city/web/assets/sprites/detail-parasol-a.png +0 -0
- package/city/web/assets/sprites/detail-parasol-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-c.png +0 -0
- package/city/web/assets/sprites/low-detail-building-d.png +0 -0
- package/city/web/assets/sprites/low-detail-building-e.png +0 -0
- package/city/web/assets/sprites/low-detail-building-f.png +0 -0
- package/city/web/assets/sprites/low-detail-building-g.png +0 -0
- package/city/web/assets/sprites/low-detail-building-h.png +0 -0
- package/city/web/assets/sprites/low-detail-building-i.png +0 -0
- package/city/web/assets/sprites/low-detail-building-j.png +0 -0
- package/city/web/assets/sprites/low-detail-building-k.png +0 -0
- package/city/web/assets/sprites/low-detail-building-l.png +0 -0
- package/city/web/assets/sprites/low-detail-building-m.png +0 -0
- package/city/web/assets/sprites/low-detail-building-n.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/assets/sprites/medidas.json +248 -0
- package/city/web/dist/city.js +939 -0
- package/city/web/dist/index.html +294 -0
- package/city/web/dist/modelos/License.txt +28 -0
- package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/dist/modelos/building-a.glb +0 -0
- package/city/web/dist/modelos/building-b.glb +0 -0
- package/city/web/dist/modelos/building-c.glb +0 -0
- package/city/web/dist/modelos/building-d.glb +0 -0
- package/city/web/dist/modelos/building-e.glb +0 -0
- package/city/web/dist/modelos/building-f.glb +0 -0
- package/city/web/dist/modelos/building-g.glb +0 -0
- package/city/web/dist/modelos/building-h.glb +0 -0
- package/city/web/dist/sprites/building-a.png +0 -0
- package/city/web/dist/sprites/building-b.png +0 -0
- package/city/web/dist/sprites/building-c.png +0 -0
- package/city/web/dist/sprites/building-d.png +0 -0
- package/city/web/dist/sprites/building-e.png +0 -0
- package/city/web/dist/sprites/building-f.png +0 -0
- package/city/web/dist/sprites/building-g.png +0 -0
- package/city/web/dist/sprites/building-h.png +0 -0
- package/city/web/dist/sprites/building-i.png +0 -0
- package/city/web/dist/sprites/building-j.png +0 -0
- package/city/web/dist/sprites/building-k.png +0 -0
- package/city/web/dist/sprites/building-l.png +0 -0
- package/city/web/dist/sprites/building-m.png +0 -0
- package/city/web/dist/sprites/building-n.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
- package/city/web/dist/sprites/catalogo.json +47 -0
- package/city/web/dist/sprites/detail-awning-wide.png +0 -0
- package/city/web/dist/sprites/detail-awning.png +0 -0
- package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
- package/city/web/dist/sprites/detail-overhang.png +0 -0
- package/city/web/dist/sprites/detail-parasol-a.png +0 -0
- package/city/web/dist/sprites/detail-parasol-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-c.png +0 -0
- package/city/web/dist/sprites/low-detail-building-d.png +0 -0
- package/city/web/dist/sprites/low-detail-building-e.png +0 -0
- package/city/web/dist/sprites/low-detail-building-f.png +0 -0
- package/city/web/dist/sprites/low-detail-building-g.png +0 -0
- package/city/web/dist/sprites/low-detail-building-h.png +0 -0
- package/city/web/dist/sprites/low-detail-building-i.png +0 -0
- package/city/web/dist/sprites/low-detail-building-j.png +0 -0
- package/city/web/dist/sprites/low-detail-building-k.png +0 -0
- package/city/web/dist/sprites/low-detail-building-l.png +0 -0
- package/city/web/dist/sprites/low-detail-building-m.png +0 -0
- package/city/web/dist/sprites/low-detail-building-n.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/dist/sprites/medidas.json +248 -0
- package/city/web/dist-hall/hall.js +264 -0
- package/city/web/index.html +294 -0
- package/city/web/package.json +18 -0
- package/city/web/sella.py +21 -0
- package/city/web/src/activity-actors.ts +47 -0
- package/city/web/src/activity.ts +172 -0
- package/city/web/src/ayuntamiento.ts +580 -0
- package/city/web/src/draw.ts +640 -0
- package/city/web/src/game-speech.ts +79 -0
- package/city/web/src/hall.ts +1867 -0
- package/city/web/src/main.ts +2225 -0
- package/city/web/src/oven.ts +124 -0
- package/city/web/src/people.ts +244 -0
- package/city/web/src/presencia.ts +193 -0
- package/city/web/src/puertas.ts +180 -0
- package/city/web/src/repo-roles.ts +59 -0
- package/city/worker/package.json +16 -0
- package/city/worker/schema.sql +117 -0
- package/city/worker/src/index.ts +702 -0
- package/city/worker/src/square.ts +218 -0
- package/city/worker/wrangler.toml +58 -0
- package/demo/ada.md +50 -0
- package/demo/bruno.md +62 -0
- package/demo/camila.md +56 -0
- package/demo/city/ada.md +40 -0
- package/demo/city/city.yml +8 -0
- package/demo/city/parcels.yml +15 -0
- package/demo/city/roads.json +4 -0
- package/demo/city/units.yml +10 -0
- package/demo/clinica/city.yml +8 -0
- package/demo/clinica/parcels.yml +15 -0
- package/demo/clinica/roads.json +4 -0
- package/demo/clinica/units.yml +9 -0
- package/demo/clinica/vera.md +46 -0
- package/demo/dante.md +54 -0
- package/demo/despacho/city.yml +8 -0
- package/demo/despacho/marta.md +46 -0
- package/demo/despacho/parcels.yml +15 -0
- package/demo/despacho/roads.json +4 -0
- package/demo/despacho/units.yml +9 -0
- package/demo/elsa.md +50 -0
- package/demo/farid.md +52 -0
- package/demo/greta.md +50 -0
- package/demo/hugo.md +49 -0
- package/demo/iris.md +49 -0
- package/demo/jonas.md +51 -0
- package/demo/kira.md +51 -0
- package/demo/luca.md +49 -0
- package/demo/parcels.yml +92 -0
- package/demo/seed.py +20 -0
- package/demo/show.py +161 -0
- package/demo/stories.py +824 -0
- package/demo/units.yml +33 -0
- package/docs/agents-first.md +99 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +231 -0
- package/docs/self-host.md +151 -0
- package/docs/testing.md +205 -0
- package/package.json +73 -0
- package/plugin/.claude-plugin/plugin.json +62 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/channel/activity-cli.ts +16 -0
- package/plugin/channel/adapter-prompts.ts +113 -0
- package/plugin/channel/adapter.js +4292 -0
- package/plugin/channel/adapter.ts +133 -0
- package/plugin/channel/bus.js +20188 -0
- package/plugin/channel/bus.ts +131 -0
- package/plugin/channel/city-config.ts +125 -0
- package/plugin/channel/claude-channel.ts +58 -0
- package/plugin/channel/cli-args.ts +75 -0
- package/plugin/channel/client.js +4200 -0
- package/plugin/channel/client.ts +38 -0
- package/plugin/channel/committee/activity.ts +256 -0
- package/plugin/channel/committee/collection.ts +154 -0
- package/plugin/channel/committee/decision.ts +175 -0
- package/plugin/channel/committee/floor.ts +135 -0
- package/plugin/channel/committee/guards.ts +30 -0
- package/plugin/channel/committee/history.ts +38 -0
- package/plugin/channel/committee/render.ts +102 -0
- package/plugin/channel/committee/service.ts +72 -0
- package/plugin/channel/committee/storage.ts +92 -0
- package/plugin/channel/committee/types.ts +163 -0
- package/plugin/channel/committee/view.ts +63 -0
- package/plugin/channel/committee-cli.ts +109 -0
- package/plugin/channel/delivery-metrics.ts +61 -0
- package/plugin/channel/delivery-queue.ts +118 -0
- package/plugin/channel/hub/activity-controller.ts +72 -0
- package/plugin/channel/hub/activity-feed.ts +169 -0
- package/plugin/channel/hub/committee-controller.ts +60 -0
- package/plugin/channel/hub/connections.ts +48 -0
- package/plugin/channel/hub/diagnostics.ts +67 -0
- package/plugin/channel/hub/envelope-validity.ts +109 -0
- package/plugin/channel/hub/envelopes.ts +66 -0
- package/plugin/channel/hub/lifecycle.ts +91 -0
- package/plugin/channel/hub/local-roads.ts +89 -0
- package/plugin/channel/hub/remote-roads.ts +165 -0
- package/plugin/channel/hub/road-controller.ts +99 -0
- package/plugin/channel/hub-client.ts +149 -0
- package/plugin/channel/local-hub.js +5995 -0
- package/plugin/channel/local-hub.ts +349 -0
- package/plugin/channel/map-reporter.ts +48 -0
- package/plugin/channel/package-lock.json +1731 -0
- package/plugin/channel/package.json +19 -0
- package/plugin/channel/protocol.ts +98 -0
- package/plugin/channel/road-cli.ts +18 -0
- package/plugin/channel/run.sh +15 -0
- package/plugin/channel/runtime/claude.ts +458 -0
- package/plugin/channel/runtime/codex-config.ts +77 -0
- package/plugin/channel/runtime/codex.ts +669 -0
- package/plugin/channel/runtime/command.ts +62 -0
- package/plugin/channel/runtime/factory.ts +15 -0
- package/plugin/channel/runtime/json-rpc.ts +154 -0
- package/plugin/channel/runtime/kimi.ts +277 -0
- package/plugin/channel/runtime/opencode.ts +248 -0
- package/plugin/channel/runtime/process.ts +84 -0
- package/plugin/channel/runtime/types.ts +45 -0
- package/plugin/channel/runtime-files.ts +73 -0
- package/plugin/channel/runtime-gateway.js +6396 -0
- package/plugin/channel/runtime-gateway.ts +358 -0
- package/plugin/channel/runtime-metrics.ts +52 -0
- package/plugin/channel/runtime-subscription.ts +180 -0
- package/plugin/channel/terminal-delivery.ts +131 -0
- package/plugin/channel/untrusted.ts +60 -0
- package/plugin/commands/committee.md +31 -0
- package/plugin/commands/exit.md +32 -0
- package/plugin/commands/goals.md +20 -0
- package/plugin/commands/join.md +18 -0
- package/plugin/commands/notice.md +20 -0
- package/plugin/commands/propose.md +19 -0
- package/plugin/commands/round.md +17 -0
- package/plugin/commands/session.md +22 -0
- package/plugin/commands/settings.md +25 -0
- package/plugin/commands/setup.md +21 -0
- package/plugin/commands/team.md +23 -0
- package/plugin/domains/custom.md +27 -0
- package/plugin/domains/finance.md +31 -0
- package/plugin/domains/healthcare.md +34 -0
- package/plugin/domains/legal.md +31 -0
- package/plugin/domains/marketing.md +33 -0
- package/plugin/domains/operations.md +31 -0
- package/plugin/domains/research.md +32 -0
- package/plugin/domains/sales.md +30 -0
- package/plugin/domains/software.md +37 -0
- package/plugin/hooks/activity.sh +6 -0
- package/plugin/hooks/digging.sh +68 -0
- package/plugin/hooks/growth.sh +42 -0
- package/plugin/hooks/hooks.json +106 -0
- package/plugin/hooks/notice-on-pr.sh +35 -0
- package/plugin/hooks/notice-on-stop.sh +80 -0
- package/plugin/hooks/notice-pending.sh +58 -0
- package/plugin/hooks/solo-en-ciudad.sh +23 -0
- package/plugin/hooks/tokens.sh +47 -0
- package/plugin/roles/examples/account-executive.md +25 -0
- package/plugin/roles/examples/ai-manager.md +34 -0
- package/plugin/roles/examples/associate.md +24 -0
- package/plugin/roles/examples/brand-lead.md +20 -0
- package/plugin/roles/examples/cfo.md +20 -0
- package/plugin/roles/examples/city-lead.md +25 -0
- package/plugin/roles/examples/clinical-director.md +26 -0
- package/plugin/roles/examples/clinical-ops.md +25 -0
- package/plugin/roles/examples/clinician.md +25 -0
- package/plugin/roles/examples/compliance.md +27 -0
- package/plugin/roles/examples/content.md +24 -0
- package/plugin/roles/examples/controller.md +27 -0
- package/plugin/roles/examples/cpto.md +41 -0
- package/plugin/roles/examples/customer-success.md +25 -0
- package/plugin/roles/examples/data-engineer.md +37 -0
- package/plugin/roles/examples/data.md +60 -0
- package/plugin/roles/examples/dev.md +37 -0
- package/plugin/roles/examples/devops.md +39 -0
- package/plugin/roles/examples/enablement.md +24 -0
- package/plugin/roles/examples/ethics.md +24 -0
- package/plugin/roles/examples/fin-analytics.md +27 -0
- package/plugin/roles/examples/health-compliance.md +25 -0
- package/plugin/roles/examples/health-data.md +26 -0
- package/plugin/roles/examples/knowledge.md +25 -0
- package/plugin/roles/examples/lifecycle.md +26 -0
- package/plugin/roles/examples/llm-engineer.md +34 -0
- package/plugin/roles/examples/managing-partner.md +20 -0
- package/plugin/roles/examples/methods.md +23 -0
- package/plugin/roles/examples/operations-lead.md +23 -0
- package/plugin/roles/examples/ops.md +24 -0
- package/plugin/roles/examples/patient-safety.md +26 -0
- package/plugin/roles/examples/performance.md +27 -0
- package/plugin/roles/examples/po.md +36 -0
- package/plugin/roles/examples/process-owner.md +23 -0
- package/plugin/roles/examples/product-design.md +39 -0
- package/plugin/roles/examples/program-manager.md +24 -0
- package/plugin/roles/examples/quality.md +25 -0
- package/plugin/roles/examples/research-director.md +24 -0
- package/plugin/roles/examples/research-ops.md +25 -0
- package/plugin/roles/examples/researcher.md +25 -0
- package/plugin/roles/examples/revenue-lead.md +25 -0
- package/plugin/roles/examples/revops.md +26 -0
- package/plugin/roles/examples/seo.md +27 -0
- package/plugin/roles/examples/specialist.md +24 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +252 -0
- package/plugin/scripts/capabilities.py +197 -0
- package/plugin/scripts/card.py +312 -0
- package/plugin/scripts/cities.py +520 -0
- package/plugin/scripts/city-env.sh +90 -0
- package/plugin/scripts/city-runtime.sh +87 -0
- package/plugin/scripts/city-session.sh +526 -0
- package/plugin/scripts/city_env.py +67 -0
- package/plugin/scripts/crecimiento.py +140 -0
- package/plugin/scripts/deliberations.py +48 -0
- package/plugin/scripts/doctor.py +172 -0
- package/plugin/scripts/domains.py +201 -0
- package/plugin/scripts/evidencia.py +64 -0
- package/plugin/scripts/find-repos.sh +111 -0
- package/plugin/scripts/gh.py +159 -0
- package/plugin/scripts/hall_protocol.py +147 -0
- package/plugin/scripts/hook_activity.py +139 -0
- package/plugin/scripts/launch.py +94 -0
- package/plugin/scripts/logs.py +98 -0
- package/plugin/scripts/pairing.py +185 -0
- package/plugin/scripts/parcels.py +132 -0
- package/plugin/scripts/read-card.py +51 -0
- package/plugin/scripts/report.py +201 -0
- package/plugin/scripts/reset.py +180 -0
- package/plugin/scripts/roads.py +230 -0
- package/plugin/scripts/roles.py +274 -0
- package/plugin/scripts/runtime_log.py +69 -0
- package/plugin/scripts/runtime_processes.py +132 -0
- package/plugin/scripts/rutas.py +83 -0
- package/plugin/scripts/seat.py +1349 -0
- package/plugin/scripts/tokens.py +194 -0
- package/plugin/scripts/trust-repos.py +59 -0
- package/plugin/scripts/ui.py +280 -0
- package/plugin/scripts/units.py +80 -0
- package/plugin/scripts/workspace.py +364 -0
- package/plugin/skills/city/SKILL.md +191 -0
- package/templates/blank.md +39 -0
- package/templates/finance.md +44 -0
- package/templates/legal.md +45 -0
- package/templates/marketing.md +48 -0
- 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,99 @@
|
|
|
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
|
+
Legacy (unchanged — every repo is an agent whose single mount is that repo):
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
repos: [nova, store-service]
|
|
38
|
+
role.nova: po
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Agent-first (adds `kind.<agent>` and `mounts.<agent>`):
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
agents: [writer, chair]
|
|
45
|
+
kind.writer: knowledge
|
|
46
|
+
mounts.writer: [~/Documents/handbook, ~/code/product/spec]
|
|
47
|
+
kind.chair: coordinator
|
|
48
|
+
runs.chair: claude
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How the cage follows a mount
|
|
52
|
+
|
|
53
|
+
The cage seals by path, so an agent's workspace is exactly what it makes
|
|
54
|
+
writable — plus the **resolved targets** of its mounts. Because seatbelt
|
|
55
|
+
resolves symlinks at the kernel, `mount_targets()` follows each symlink to its
|
|
56
|
+
real destination and the launcher passes those to `cage.py --mounts`. A mount
|
|
57
|
+
that resolves inside a sealed root (`~/.ssh`, the broker store, …) is refused,
|
|
58
|
+
never honoured: the security invariant holds under the new model, and
|
|
59
|
+
`bin/test-cage.py` proves the writable-mount / sealed-secret split against the
|
|
60
|
+
live kernel.
|
|
61
|
+
|
|
62
|
+
## Managing mounts
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agents-city agents list --data <city> --card <card> # list normalised agents
|
|
66
|
+
agents-city agents mount --data <city> --agent writer --src ~/Documents/handbook
|
|
67
|
+
agents-city agents mounts --data <city> --agent writer # what is mounted
|
|
68
|
+
agents-city agents unmount --data <city> --agent writer --name handbook
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
At session start the launcher runs `workspace.py sync` for each agent, which
|
|
72
|
+
creates the workspace and materialises every declared mount (skipping a missing
|
|
73
|
+
source with a warning rather than aborting the city).
|
|
74
|
+
|
|
75
|
+
## Growth without git — `crecimiento.py`
|
|
76
|
+
|
|
77
|
+
A house grows by what its agent actually does, not by pull requests alone:
|
|
78
|
+
|
|
79
|
+
- **code** — floors = merged PRs, bricks = commits (an injected git counter).
|
|
80
|
+
- **knowledge** — floors = documents in the workspace, bricks = recent edits.
|
|
81
|
+
- **coordinator** — floors = recorded decisions, bricks = notices.
|
|
82
|
+
|
|
83
|
+
Every counter returns the same `{floors, bricks, activity30, signal}` shape, so
|
|
84
|
+
a person with no git at all now has a house that grows as their knowledge does.
|
|
85
|
+
|
|
86
|
+
## Avatars — `avatar.py`
|
|
87
|
+
|
|
88
|
+
Each agent gets a deterministic, self-contained SVG identicon from its name
|
|
89
|
+
(same name → same face), with the border tinted by kind. No network, no
|
|
90
|
+
library, no external asset, so it drops into the Hall under its strict CSP —
|
|
91
|
+
`avatar.data_uri(name, kind)` is ready for an `<img src>`.
|
|
92
|
+
|
|
93
|
+
## Presence and "thinking"
|
|
94
|
+
|
|
95
|
+
The activity pipeline already emits on `UserPromptSubmit` (a turn begins) and
|
|
96
|
+
`Stop` (it ends), so "active since the last prompt without a stop" is derivable
|
|
97
|
+
from the existing feed — the Hall renders it. This document's generators
|
|
98
|
+
(avatars, growth) provide the data; wiring them into the Hall view is the
|
|
99
|
+
rendering layer's job.
|
package/docs/glossary.md
ADDED
|
@@ -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.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
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 repo windows launch under a generated macOS seatbelt
|
|
18
|
+
profile:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
sandbox-exec -f ~/.agents-city/.runtime/cage/<window>.sb <runtime …>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The profile reads top to bottom: allow everything, deny all writes, re-allow
|
|
25
|
+
the working set, then seal the secrets — reads and writes both. The semantics
|
|
26
|
+
(last matching rule wins; children and grandchildren inherit) were verified on
|
|
27
|
+
a real machine before a line of it was written, and `bin/test-cage.py` re-proves
|
|
28
|
+
them live on every run where seatbelt exists.
|
|
29
|
+
|
|
30
|
+
Inside the cage a window **can**: work freely in its own repo, run builds,
|
|
31
|
+
reach the network, keep its runtime state (`~/.claude`, `~/.codex`,
|
|
32
|
+
`~/.agents-city`, build caches). It is never asked a question — yolo intact.
|
|
33
|
+
|
|
34
|
+
Inside the cage a window **cannot**: read or write `~/.ssh`, `~/.aws`,
|
|
35
|
+
`~/.kube`, `~/.gnupg`, `~/.docker`, `~/.config/gcloud`, `~/.config/gh`,
|
|
36
|
+
`~/.git-credentials`, `~/.netrc`, `~/.pgpass`, cargo credentials, any remote
|
|
37
|
+
road `.env` under `~/.claude/channels/`, or the broker's state; nor write
|
|
38
|
+
anywhere outside its repo and the allowed runtime/cache set — other repos on
|
|
39
|
+
the machine included.
|
|
40
|
+
|
|
41
|
+
Dials, all environment variables read at launch:
|
|
42
|
+
|
|
43
|
+
| Variable | Effect |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `CITY_CAGE=0` | launch every window uncaged — exactly the old behaviour |
|
|
46
|
+
| `CITY_CAGE_DENY=a:b` | extra paths to seal (e.g. `~/.npmrc`, see below) |
|
|
47
|
+
| `CITY_CAGE_ALLOW_WRITE=a:b` | extra writable roots for unusual toolchains |
|
|
48
|
+
|
|
49
|
+
On Linux or any machine without `sandbox-exec`, the prefix is empty and
|
|
50
|
+
nothing changes. The seat is deliberately never caged: it is the permissioned,
|
|
51
|
+
non-yolo side that holds the road token and asks the owner.
|
|
52
|
+
|
|
53
|
+
Codex uses exactly one confinement layer: its native `workspace-write` sandbox
|
|
54
|
+
(network enabled only in auto mode). It is deliberately not wrapped in the
|
|
55
|
+
outer seatbelt profile because Codex MCPs such as `node_repl` create their own
|
|
56
|
+
sandboxed worker; macOS rejects that second `sandbox_apply` when the entire
|
|
57
|
+
gateway already runs under seatbelt. `CITY_CAGE=0` deliberately changes Codex
|
|
58
|
+
to `dangerFullAccess` and launches the other providers without their outer
|
|
59
|
+
cage.
|
|
60
|
+
|
|
61
|
+
At startup, the Codex gateway also reads the effective MCP registry through the
|
|
62
|
+
Codex CLI. An enabled stdio MCP whose executable is provably absent is disabled
|
|
63
|
+
only through a process-local `-c mcp_servers.<name>.enabled=false` override for
|
|
64
|
+
app-server and its TUI. The owner's `~/.codex/config.toml` is never rewritten;
|
|
65
|
+
healthy, URL-based and uncertain integrations are left alone. Each scoped
|
|
66
|
+
disable is recorded as `codex.mcp.unavailable.disabled` without MCP environment
|
|
67
|
+
values.
|
|
68
|
+
|
|
69
|
+
## Layer 2 — the credential broker (`plugin/scripts/broker.py`)
|
|
70
|
+
|
|
71
|
+
A caged window cannot read the `gh` token — that is the cage working. But
|
|
72
|
+
opening a pull request is legitimate work, so the window asks a small process
|
|
73
|
+
the owner runs outside every cage, and that process acts with the real
|
|
74
|
+
credentials after validating the request:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
CITY_BROKER=1 agents-city seat # start the session with a broker
|
|
78
|
+
python3 plugin/scripts/broker.py call pr --title "Add X" --body "why"
|
|
79
|
+
python3 plugin/scripts/broker.py call push
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- One token per window, minted at session start, **bound to one repo**: a
|
|
83
|
+
stolen token cannot choose a different target.
|
|
84
|
+
- Only the SHA-256 of a token is stored; the token itself travels as a file
|
|
85
|
+
path whose read is the single exception the window's cage re-allows.
|
|
86
|
+
- The broker refuses any action on the default branch, always. Pushes are
|
|
87
|
+
`git push origin HEAD` — no force, no refspec.
|
|
88
|
+
- The broker is opt-in (`CITY_BROKER=1`) while it covers only `pr` and `push`;
|
|
89
|
+
stop it with `broker.py stop --data <city-dir>`.
|
|
90
|
+
|
|
91
|
+
## Layer 3 — the audit chain, with an evidence vocabulary
|
|
92
|
+
|
|
93
|
+
Every broker request — served or refused — appends one line to
|
|
94
|
+
`~/.agents-city/.runtime/broker/<id>/audit.log`, and each line carries the
|
|
95
|
+
SHA-256 of the previous one. The cage denies repo windows the whole directory,
|
|
96
|
+
so an agent cannot rewrite its own history, and `broker.py verify` catches a
|
|
97
|
+
single flipped byte. `bin/test-broker.py` proves both directions.
|
|
98
|
+
|
|
99
|
+
A hash chain proves a line was not altered; it says nothing about what the line
|
|
100
|
+
proves. So every line also carries an evidence state (`plugin/scripts/evidencia.py`):
|
|
101
|
+
`enforced` (a control decided the outcome), `attribution-only`, `unattributed`,
|
|
102
|
+
`unknown`, `ambiguous`, `unsupported`. Two rules hold wherever it is read —
|
|
103
|
+
`unknown` never means allowed, and `ambiguous` is never resolved by picking a
|
|
104
|
+
candidate. A missing binding is `unknown`, which authorises nothing; only
|
|
105
|
+
`enforced` is proof a control fired.
|
|
106
|
+
|
|
107
|
+
## Layer 4 — path containment, resolved twice
|
|
108
|
+
|
|
109
|
+
The cage's write-allow set and the broker's repo binding both trust paths, and
|
|
110
|
+
`plugin/scripts/rutas.py` answers the same question for both: it resolves a path
|
|
111
|
+
through the deepest ancestor that exists — so a missing leaf under a symlinked
|
|
112
|
+
parent still lands on the real target — and refuses a path both when it sits
|
|
113
|
+
inside a sealed root and when it *covers* one. The cage's own guarantee is the
|
|
114
|
+
ordering (the secret-deny block is emitted last and wins), so a covering
|
|
115
|
+
writable root is safe; what containment adds is refusing a repo rooted on the
|
|
116
|
+
credential store and dropping an owner `CITY_CAGE_ALLOW_WRITE` that lands inside
|
|
117
|
+
a seal. `bin/test-rutas.py` drives the primitive directly.
|
|
118
|
+
|
|
119
|
+
## Layer 5 — road admission as a gate graph
|
|
120
|
+
|
|
121
|
+
An inbound road message is admitted by an ordered gate graph
|
|
122
|
+
(`plugin/scripts/admision.py`), not a boolean: address → road → sender →
|
|
123
|
+
payload, first blocker decisive. It returns a stable reason code
|
|
124
|
+
(`road_missing`, `sender_not_paired`, …) and, per gate, a redacted diagnostic —
|
|
125
|
+
counts and opaque ids, never a raw allowlist entry — so a refusal is
|
|
126
|
+
explainable without leaking who is allowed. A road that matches but whose sender
|
|
127
|
+
does not is an explicit block; an unknown sender under pairing yields
|
|
128
|
+
`pairing-required` rather than a silent drop.
|
|
129
|
+
|
|
130
|
+
## Layer 6 — road pairing
|
|
131
|
+
|
|
132
|
+
An unknown city earns the right to write to a seat through a short code
|
|
133
|
+
(`plugin/scripts/pairing.py`): eight characters from an unambiguous alphabet, a
|
|
134
|
+
one-hour TTL, at most three pending per city, and an opaque approval id so
|
|
135
|
+
tooling never echoes the human code. The code is revealed once per sender per
|
|
136
|
+
window, so re-messaging cannot re-spam it. Approving grants exactly one thing —
|
|
137
|
+
permission for that address to write to this seat — never road membership, never
|
|
138
|
+
chair authority. The store is consulted only under the pairing policy; it can
|
|
139
|
+
never widen an existing allowlist.
|
|
140
|
+
|
|
141
|
+
## Layer 7 — untrusted road text
|
|
142
|
+
|
|
143
|
+
Text arriving over a road is wrapped once, on arrival, in an unforgeable
|
|
144
|
+
boundary (`plugin/channel/untrusted.ts`): the opening marker carries a fresh
|
|
145
|
+
random id per wrap, so a message that pastes its own closing marker cannot guess
|
|
146
|
+
the id and cannot smuggle the rest as trusted text. Chat-template role tokens
|
|
147
|
+
(`<|im_start|>`, `[INST]`, `<start_of_turn>`, …) are defanged first, so text
|
|
148
|
+
cannot forge a synthetic system or assistant turn on a self-hosted backend. It
|
|
149
|
+
is defence in depth for the seat, not a promise the model obeys the boundary.
|
|
150
|
+
|
|
151
|
+
## Host-bound secrets — the broker without handing over the key
|
|
152
|
+
|
|
153
|
+
A caged window cannot read an API token, and the broker does not hand it one.
|
|
154
|
+
The owner binds a secret to an exact host set (`broker.py secret-set OPENAI
|
|
155
|
+
--value … --allow-host api.openai.com`; no wildcards, no ports), and a window
|
|
156
|
+
asks the broker to make the call carrying the secret by *name*
|
|
157
|
+
(`broker.py call fetch --secret OPENAI --host api.openai.com --path /v1/…`). The
|
|
158
|
+
broker injects the value only when the target host matches a bound one, and
|
|
159
|
+
never returns it. A leaked transcript or `ps` line holds the request, never the
|
|
160
|
+
credential.
|
|
161
|
+
|
|
162
|
+
## The Hall protocol — writing to the seat from a page
|
|
163
|
+
|
|
164
|
+
When the local web Hall is built, it talks to the bus that serves it over one
|
|
165
|
+
same-origin socket, on the contract in `plugin/scripts/hall_protocol.py`: three
|
|
166
|
+
frames (`req`/`res`/`event`), a per-connection monotonic sequence, and the rule
|
|
167
|
+
that events are never replayed — on a gap the client re-fetches a snapshot. The
|
|
168
|
+
method set is closed (`os.system` is not a method); writing to the seat is one
|
|
169
|
+
call with a queue mode (`start`/`steer`/`queue`/`note`) so an idle and a busy
|
|
170
|
+
seat are the same call. Loopback is a pairing convenience, never a substitute
|
|
171
|
+
for the seat's own admission.
|
|
172
|
+
|
|
173
|
+
## Config migrations — `doctor`
|
|
174
|
+
|
|
175
|
+
The runtime reads one config schema. Every change that can invalidate an
|
|
176
|
+
existing config ships a migration in `plugin/scripts/doctor.py` that detects the
|
|
177
|
+
old shape, explains it in one line, writes a timestamped backup, and rewrites to
|
|
178
|
+
canonical form. Migrations are idempotent and ordered, so a file needing several
|
|
179
|
+
converges in one pass and a canonical file is left untouched. `--fix` writes;
|
|
180
|
+
the default is a dry-run report.
|
|
181
|
+
|
|
182
|
+
## The ratchet
|
|
183
|
+
|
|
184
|
+
`bin/test-security.py` is not a unit suite but a firewall of cross-cutting
|
|
185
|
+
invariants: the secret set always contains `~/.ssh`/`~/.git-credentials`/gh, the
|
|
186
|
+
seal block is always emitted after the write-allow block, `unknown` evidence
|
|
187
|
+
never authorises, an admission decision never carries a raw allowlist entry, the
|
|
188
|
+
Hall method set stays closed, and the launcher never wraps the seat window in
|
|
189
|
+
the cage. Each past sharp edge becomes a line here, and the line stays.
|
|
190
|
+
|
|
191
|
+
## Scope — the conscience stays inside the city
|
|
192
|
+
|
|
193
|
+
Installing the plugin does not enrol every Claude session on the machine.
|
|
194
|
+
Every hook sources `plugin/hooks/solo-en-ciudad.sh` first: outside a city
|
|
195
|
+
runtime — no `CITY_BUS_ACTOR`, the identity only `city-session.sh` sets — the
|
|
196
|
+
hook answers `{}` and leaves. No review passes, no digging notes, no notice
|
|
197
|
+
judgements, no token counters in plain conversations. The MCP server already
|
|
198
|
+
declared itself inactive without an actor identity; this extends the same rule
|
|
199
|
+
to the hooks, which were the remaining machine-wide surface.
|
|
200
|
+
|
|
201
|
+
The machine-wide conscience still exists as an explicit choice:
|
|
202
|
+
`CITY_HOOKS=everywhere`, in the environment or in
|
|
203
|
+
`~/.claude/channels/city-bus/.env`. Second-order effects are opted into, never
|
|
204
|
+
discovered. `bin/test-contracts.py` holds both sides: every hook is mute
|
|
205
|
+
outside a city, and both opt-in paths open the gate.
|
|
206
|
+
|
|
207
|
+
## What this does NOT do, in writing
|
|
208
|
+
|
|
209
|
+
- **Prompt injection still exists.** The cage bounds what it is worth.
|
|
210
|
+
- **Keychain credentials travel over IPC, not file reads.** A jailed `git
|
|
211
|
+
push` still works when GitHub credentials live in the macOS keychain; the
|
|
212
|
+
broker and forge-side protected branches are the guard there. If you keep
|
|
213
|
+
`~/.git-credentials` in plain text, move it: `git config --global
|
|
214
|
+
credential.helper osxkeychain`.
|
|
215
|
+
- **Outbound network stays open** — that is what keeps yolo useful. A window
|
|
216
|
+
can still exfiltrate what it can already see: its own repo.
|
|
217
|
+
- **Everything runs as your OS user.** The cage narrows file reach; it is not
|
|
218
|
+
hostile-process isolation. Untrusted code still belongs in a separate user,
|
|
219
|
+
VM or container.
|
|
220
|
+
- `~/.npmrc` stays readable by default because denying it breaks `npm install`
|
|
221
|
+
for owners with a registry config. If yours holds tokens, add it to
|
|
222
|
+
`CITY_CAGE_DENY`.
|
|
223
|
+
|
|
224
|
+
## Where this lands against an exposed-gateway agent
|
|
225
|
+
|
|
226
|
+
The criticisms that stuck to OpenClaw-class setups were: reachable from the
|
|
227
|
+
network, broad host access, credentials in reach of the model. Here: nothing
|
|
228
|
+
listens beyond `127.0.0.1`, writes are kernel-bounded per repo, and the
|
|
229
|
+
credentials a hijacked window would want are either unreadable (cage) or never
|
|
230
|
+
held (broker). The fourth problem — the model reads untrusted text — remains
|
|
231
|
+
everyone's, and the three layers above exist to make it survivable.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Self-hosting remote roads
|
|
2
|
+
|
|
3
|
+
Local cities and local roads need no external server: each city automatically
|
|
4
|
+
starts its loopback WebSocket hub. Self-host this transport only when two city
|
|
5
|
+
seats on different machines must exchange messages.
|
|
6
|
+
|
|
7
|
+
The Cloudflare Worker is a narrow relay:
|
|
8
|
+
|
|
9
|
+
- a token authenticates one owner prefix;
|
|
10
|
+
- each running seat registers as its stable `owner/city` address;
|
|
11
|
+
- the relay carries the same `agents-city-bus/2` envelope as the local hub;
|
|
12
|
+
- only `scope: road` envelopes from `seat` to `seat` are accepted;
|
|
13
|
+
- the channel still filters senders and recipients through that city's
|
|
14
|
+
`roads.json`;
|
|
15
|
+
- offline direct messages wait for at most 72 hours, up to 200 per city;
|
|
16
|
+
- message bodies are not logged unless `LOG_CONTENT = "1"` is deliberately set.
|
|
17
|
+
|
|
18
|
+
The relay does not distribute configuration, skills, files or authority. It
|
|
19
|
+
cannot carry an internal committee envelope.
|
|
20
|
+
|
|
21
|
+
## 1. Deploy the relay
|
|
22
|
+
|
|
23
|
+
Set the Cloudflare account id in `bus/worker/wrangler.toml`, then:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd bus
|
|
27
|
+
npx --yes wrangler@4 login
|
|
28
|
+
./scripts/deploy.sh
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The deploy script creates the `TOKENS` KV namespace when necessary and deploys
|
|
32
|
+
the Worker plus its SQLite-backed Durable Object. Keep the URL it prints.
|
|
33
|
+
|
|
34
|
+
## 2. Mint one token per owner
|
|
35
|
+
|
|
36
|
+
An owner can run several cities with the same token because every address shares
|
|
37
|
+
that owner's prefix:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd bus
|
|
41
|
+
./scripts/mint-token.sh joseluiscases
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Only the SHA-256 hash is stored in KV and the token is printed once. Store it
|
|
45
|
+
privately. It permits addresses such as `joseluiscases/home` and
|
|
46
|
+
`joseluiscases/product`, but cannot register as another owner.
|
|
47
|
+
|
|
48
|
+
An optional `--to` restriction can narrow the relay-level destinations:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
./scripts/mint-token.sh joseluiscases --to alice,bob/research
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This is defence in depth. `roads.json` remains the city-level allowlist.
|
|
55
|
+
|
|
56
|
+
## 3. Configure each machine, not each city
|
|
57
|
+
|
|
58
|
+
The installed plugin accepts the remote roads URL and token. For a manual
|
|
59
|
+
development setup, use:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
~/.claude/channels/city-bus/.env
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
with:
|
|
66
|
+
|
|
67
|
+
```dotenv
|
|
68
|
+
CITY_BUS_URL=https://city-bus.example.workers.dev
|
|
69
|
+
CITY_BUS_TOKEN=rb_...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Do not put `CITY_BUS_AGENT` in new configuration. The seat derives its own
|
|
73
|
+
`owner/city` address from the selected city at runtime, which is what lets
|
|
74
|
+
several local cities use one plugin installation safely.
|
|
75
|
+
|
|
76
|
+
The token can instead live in the macOS keychain under the service
|
|
77
|
+
`city@agents-city`; that is where the plugin configuration stores sensitive
|
|
78
|
+
values.
|
|
79
|
+
|
|
80
|
+
## 4. Exchange city invitations
|
|
81
|
+
|
|
82
|
+
Authentication makes a seat reachable; a road makes it allowed. Each side sends
|
|
83
|
+
the other its public invitation and accepts it independently:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# machine A
|
|
87
|
+
agents-city road invite product > product.invitation.json
|
|
88
|
+
agents-city road connect product research.invitation.json
|
|
89
|
+
|
|
90
|
+
# machine B
|
|
91
|
+
agents-city road invite research > research.invitation.json
|
|
92
|
+
agents-city road connect research product.invitation.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Transfer invitation files by any ordinary channel. They contain no token:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"version": 1,
|
|
100
|
+
"id": "city_…",
|
|
101
|
+
"name": "product",
|
|
102
|
+
"owner": "joseluiscases",
|
|
103
|
+
"address": "joseluiscases/product"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Open both seats. `bus_roster` — or `/city:team`, kept as a compatibility command
|
|
108
|
+
name — reports only explicit roads and whether their destination seats are
|
|
109
|
+
online. Messages to an offline remote city are queued by the relay.
|
|
110
|
+
|
|
111
|
+
Validate the relay and its durable typed queue locally before deploying:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
cd bus/worker
|
|
115
|
+
npm ci
|
|
116
|
+
npm run typecheck
|
|
117
|
+
npm run test:local
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Trust boundary
|
|
121
|
+
|
|
122
|
+
A valid road grants permission to deliver text, not permission to act. The seat
|
|
123
|
+
must treat incoming text as untrusted context, verify claims locally and ask for
|
|
124
|
+
the same user confirmation that would be required without the road.
|
|
125
|
+
|
|
126
|
+
Repo support windows authenticate only as members of their local committee bus.
|
|
127
|
+
They receive no road tools or remote URL/token; attempts to invoke road commands
|
|
128
|
+
are rejected by the hub ACL. Never copy an inbound request into an autonomous
|
|
129
|
+
repo agent as an instruction; open a bounded evidence request and bring
|
|
130
|
+
consequential actions back to the seat.
|
|
131
|
+
|
|
132
|
+
These local credentials enforce routing, not hostile-process isolation: every
|
|
133
|
+
runtime normally executes as the same OS user. Use separate OS/container users
|
|
134
|
+
for untrusted code. Remote owner tokens remain genuine network credentials and
|
|
135
|
+
must never be exposed to repo runtimes.
|
|
136
|
+
|
|
137
|
+
## Optional map deployment
|
|
138
|
+
|
|
139
|
+
The visual map is independent of road delivery. A city can run entirely without
|
|
140
|
+
it, and a map outage must not stop the relay.
|
|
141
|
+
|
|
142
|
+
To deploy a map, build `city/web`, create the D1 database from
|
|
143
|
+
`city/worker/schema.sql`, seed one chosen city with `city/scripts/seed.py`, set a
|
|
144
|
+
long `CITY_SECRET`, then deploy `city/worker`. Put an identity proxy such as
|
|
145
|
+
Cloudflare Access in front of it and set `REQUIRE_ACCESS = "1"`; the map exposes
|
|
146
|
+
the shape and activity of the selected codebase and should not be public by
|
|
147
|
+
default.
|
|
148
|
+
|
|
149
|
+
If the relay should report delivery metadata to that map, configure `CITY_URL`
|
|
150
|
+
in `bus/worker/wrangler.toml` and the same `CITY_SECRET` in both Workers. The
|
|
151
|
+
relay works normally when either value is absent.
|