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.
- package/.claude-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2226 -0
- package/README.md +2196 -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 +119 -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/doctor +3 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +498 -0
- package/bin/logs +3 -0
- package/bin/navegador.mjs +243 -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 +1579 -0
- package/bin/setup.py +753 -0
- package/bin/shortcut +3 -0
- package/bin/skills +3 -0
- package/bin/test +133 -0
- package/bin/test-actualiza.py +130 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-atajos.py +301 -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 +374 -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 +551 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +139 -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-navegador.py +140 -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 +1598 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1251 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +251 -0
- package/bin/testlib.py +118 -0
- package/bin/tokens.py +14 -0
- package/bin/update +3 -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 +1363 -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 +1959 -0
- package/city/web/src/main.ts +2252 -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 +106 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +265 -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/actualiza.py +198 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +492 -0
- package/plugin/scripts/capabilities.py +210 -0
- package/plugin/scripts/card.py +322 -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 +552 -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 +294 -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 +55 -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 +1696 -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 +561 -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
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The broker: real git against a bare origin, a stubbed gh, a live server.
|
|
3
|
+
|
|
4
|
+
Everything runs below a throwaway HOME. The happy paths use a REAL `git push`
|
|
5
|
+
to a local bare remote — no network — and a `gh` stub planted on PATH that
|
|
6
|
+
records its arguments. The unhappy paths are the product: work on the default
|
|
7
|
+
branch refused, a missing token 401, a wrong token 403, a tampered audit line
|
|
8
|
+
caught by the chain, and a stopped broker that leaves no orphan behind.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
|
|
21
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
+
RAIZ = os.path.dirname(AQUI)
|
|
23
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
24
|
+
sys.path.insert(0, AQUI)
|
|
25
|
+
|
|
26
|
+
import broker # noqa: E402
|
|
27
|
+
from testlib import afirma, comprueba, detiene_proceso, resumen # noqa: E402
|
|
28
|
+
|
|
29
|
+
BROKER = os.path.join(RAIZ, "plugin", "scripts", "broker.py")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _git(repo, *args):
|
|
33
|
+
return subprocess.run(["git", "-C", repo, *args], capture_output=True, text=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def prepara():
|
|
37
|
+
base = tempfile.mkdtemp(prefix="agents-city-broker-")
|
|
38
|
+
os.environ["AGENTS_CITY_HOME"] = os.path.join(base, "app")
|
|
39
|
+
datos = os.path.join(base, "city")
|
|
40
|
+
os.makedirs(datos)
|
|
41
|
+
origen = os.path.join(base, "origin.git")
|
|
42
|
+
subprocess.run(["git", "init", "-q", "--bare", origen], check=True)
|
|
43
|
+
repo = os.path.join(base, "repo")
|
|
44
|
+
subprocess.run(["git", "init", "-q", "-b", "main", repo], check=True)
|
|
45
|
+
_git(repo, "config", "user.email", "t@t")
|
|
46
|
+
_git(repo, "config", "user.name", "t")
|
|
47
|
+
with open(os.path.join(repo, "f.txt"), "w") as f:
|
|
48
|
+
f.write("a\n")
|
|
49
|
+
_git(repo, "add", "f.txt")
|
|
50
|
+
_git(repo, "commit", "-qm", "first")
|
|
51
|
+
_git(repo, "remote", "add", "origin", origen)
|
|
52
|
+
_git(repo, "push", "-q", "origin", "main")
|
|
53
|
+
stubs = os.path.join(base, "stubs")
|
|
54
|
+
os.makedirs(stubs)
|
|
55
|
+
with open(os.path.join(stubs, "gh"), "w") as f:
|
|
56
|
+
f.write('#!/bin/sh\necho "$@" > "$GH_STUB_LOG"\n'
|
|
57
|
+
'echo "https://github.com/x/y/pull/7"\n')
|
|
58
|
+
os.chmod(os.path.join(stubs, "gh"), 0o755)
|
|
59
|
+
return base, datos, repo, origen, stubs
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def pide(url, ruta, token, cuerpo=None):
|
|
63
|
+
peticion = urllib.request.Request(
|
|
64
|
+
url + ruta, data=json.dumps(cuerpo or {}).encode(), method="POST",
|
|
65
|
+
headers={"Authorization": f"Bearer {token}"} if token else {})
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(peticion, timeout=10) as r:
|
|
68
|
+
return r.status, json.loads(r.read())
|
|
69
|
+
except urllib.error.HTTPError as e:
|
|
70
|
+
return e.code, json.loads(e.read() or b"{}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def arranca(datos, entorno):
|
|
74
|
+
proceso = subprocess.Popen([sys.executable, BROKER, "serve", "--data", datos],
|
|
75
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
76
|
+
text=True, env=entorno)
|
|
77
|
+
limite = time.monotonic() + 10
|
|
78
|
+
while time.monotonic() < limite:
|
|
79
|
+
url = broker.url_de(datos)
|
|
80
|
+
if url:
|
|
81
|
+
return proceso, url
|
|
82
|
+
time.sleep(0.05)
|
|
83
|
+
proceso.kill()
|
|
84
|
+
# Never a blind failure: whatever the child said is the whole diagnosis,
|
|
85
|
+
# and CI is the one place nobody can re-run it by hand to find out.
|
|
86
|
+
salida, error = proceso.communicate(timeout=5)
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
f"the broker never wrote its endpoint\n stdout: {salida.strip()}\n"
|
|
89
|
+
f" stderr: {error.strip()}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def todo():
|
|
94
|
+
base, datos, repo, origen, stubs = prepara()
|
|
95
|
+
entorno = dict(os.environ, PATH=stubs + os.pathsep + os.environ["PATH"],
|
|
96
|
+
GH_STUB_LOG=os.path.join(base, "gh.log"))
|
|
97
|
+
proceso = None
|
|
98
|
+
try:
|
|
99
|
+
ficha = broker.acuna(datos, "web", repo, solo_fichero=True)
|
|
100
|
+
afirma("mint writes the token file with 0600",
|
|
101
|
+
oct(os.stat(ficha).st_mode & 0o777) == "0o600", ficha)
|
|
102
|
+
token = open(ficha).read().strip()
|
|
103
|
+
guardados = json.load(open(os.path.join(broker.estado_de(datos), "tokens.json")))
|
|
104
|
+
afirma("only the hash of the token is stored, never the token",
|
|
105
|
+
token not in json.dumps(guardados) and len(guardados) == 1)
|
|
106
|
+
try:
|
|
107
|
+
broker.acuna(datos, "x", os.path.join(base, "not-a-repo"))
|
|
108
|
+
afirma("minting against a non-repo is refused", False)
|
|
109
|
+
except ValueError:
|
|
110
|
+
afirma("minting against a non-repo is refused", True)
|
|
111
|
+
|
|
112
|
+
proceso, url = arranca(datos, entorno)
|
|
113
|
+
|
|
114
|
+
estado, _ = pide(url, "/v1/push", token)
|
|
115
|
+
comprueba("push on the default branch is refused with 422", estado, 422)
|
|
116
|
+
estado, _ = pide(url, "/v1/pr", token, {"title": "x"})
|
|
117
|
+
comprueba("a PR from the default branch is refused too", estado, 422)
|
|
118
|
+
|
|
119
|
+
_git(repo, "checkout", "-qb", "feature")
|
|
120
|
+
with open(os.path.join(repo, "f.txt"), "w") as f:
|
|
121
|
+
f.write("b\n")
|
|
122
|
+
_git(repo, "commit", "-qam", "change")
|
|
123
|
+
|
|
124
|
+
estado, cuerpo = pide(url, "/v1/push", token)
|
|
125
|
+
comprueba("push on a feature branch succeeds", estado, 200)
|
|
126
|
+
afirma("the bare origin actually received the branch",
|
|
127
|
+
_git(origen, "rev-parse", "feature").returncode == 0)
|
|
128
|
+
comprueba("the response names the branch it pushed", cuerpo.get("branch"), "feature")
|
|
129
|
+
|
|
130
|
+
estado, cuerpo = pide(url, "/v1/pr", token, {"title": "Add b", "body": "why"})
|
|
131
|
+
comprueba("gh runs and the PR URL comes back", estado, 200)
|
|
132
|
+
comprueba("the URL is gh's answer, verbatim",
|
|
133
|
+
cuerpo.get("url"), "https://github.com/x/y/pull/7")
|
|
134
|
+
registrado = open(os.path.join(base, "gh.log")).read()
|
|
135
|
+
afirma("gh received the title the window asked for", "Add b" in registrado, registrado)
|
|
136
|
+
|
|
137
|
+
estado, _ = pide(url, "/v1/pr", token, {"title": ""})
|
|
138
|
+
comprueba("a PR without a title is refused", estado, 422)
|
|
139
|
+
estado, _ = pide(url, "/v1/push", "")
|
|
140
|
+
comprueba("no token at all is a 401", estado, 401)
|
|
141
|
+
estado, _ = pide(url, "/v1/push", "cb_" + "0" * 48)
|
|
142
|
+
comprueba("a wrong token is a 403", estado, 403)
|
|
143
|
+
estado, _ = pide(url, "/v1/nothing", token)
|
|
144
|
+
comprueba("an unknown verb is a 404", estado, 404)
|
|
145
|
+
|
|
146
|
+
registro = os.path.join(broker.estado_de(datos), "audit.log")
|
|
147
|
+
lineas = open(registro).read().splitlines()
|
|
148
|
+
afirma("every request, refused ones included, landed in the audit log",
|
|
149
|
+
sum(1 for l in lineas if '"/v1/' in l) >= 7, f"{len(lineas)} lines")
|
|
150
|
+
comprueba("the audit chain verifies intact", broker.verifica(broker.estado_de(datos)), 0)
|
|
151
|
+
manipuladas = list(lineas)
|
|
152
|
+
falseada = json.loads(manipuladas[2])
|
|
153
|
+
falseada["ok"] = not falseada["ok"]
|
|
154
|
+
manipuladas[2] = json.dumps(falseada, sort_keys=True)
|
|
155
|
+
with open(registro, "w") as f:
|
|
156
|
+
f.write("\n".join(manipuladas) + "\n")
|
|
157
|
+
afirma("one rewritten entry breaks the chain at the next line",
|
|
158
|
+
broker.verifica(broker.estado_de(datos)) == 4,
|
|
159
|
+
f"broken at {broker.verifica(broker.estado_de(datos))}")
|
|
160
|
+
|
|
161
|
+
afirma("stop finds the pid it published and signals it", broker.para(datos))
|
|
162
|
+
try:
|
|
163
|
+
proceso.wait(timeout=5)
|
|
164
|
+
afirma("the broker is gone, no orphan left", detiene_proceso(proceso.pid))
|
|
165
|
+
except subprocess.TimeoutExpired:
|
|
166
|
+
afirma("the broker is gone, no orphan left", False)
|
|
167
|
+
proceso = None
|
|
168
|
+
comprueba("a stopped broker publishes no url", broker.url_de(datos), "")
|
|
169
|
+
finally:
|
|
170
|
+
if proceso:
|
|
171
|
+
proceso.kill()
|
|
172
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
173
|
+
os.environ.pop("AGENTS_CITY_HOME", None)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def secretos_y_evidencia():
|
|
177
|
+
from evidencia import Evidencia
|
|
178
|
+
|
|
179
|
+
base = tempfile.mkdtemp(prefix="agents-city-broker-")
|
|
180
|
+
os.environ["AGENTS_CITY_HOME"] = os.path.join(base, "app")
|
|
181
|
+
try:
|
|
182
|
+
datos = os.path.join(base, "city")
|
|
183
|
+
os.makedirs(datos)
|
|
184
|
+
estado = broker.estado_de(datos)
|
|
185
|
+
|
|
186
|
+
# Binding rules: a host is required, and wildcards/ports are refused.
|
|
187
|
+
for malo in ("*.example.com", "api.example.com:443", "a/b"):
|
|
188
|
+
try:
|
|
189
|
+
broker.guarda_secreto(datos, "K", "v", [malo])
|
|
190
|
+
afirma(f"host '{malo}' is refused", False)
|
|
191
|
+
except ValueError:
|
|
192
|
+
afirma(f"host '{malo}' is refused", True)
|
|
193
|
+
try:
|
|
194
|
+
broker.guarda_secreto(datos, "K", "v", [])
|
|
195
|
+
afirma("a secret with no host is refused", False)
|
|
196
|
+
except ValueError:
|
|
197
|
+
afirma("a secret with no host is refused", True)
|
|
198
|
+
|
|
199
|
+
broker.guarda_secreto(datos, "OPENAI", "sk-REAL-SECRET", ["api.openai.com"])
|
|
200
|
+
valor, motivo = broker._host_permitido(estado, "OPENAI", "api.openai.com")
|
|
201
|
+
comprueba("a bound host resolves the value", valor, "sk-REAL-SECRET")
|
|
202
|
+
_, motivo = broker._host_permitido(estado, "OPENAI", "evil.example.com")
|
|
203
|
+
afirma("an unbound host is refused with a reason", motivo is not None)
|
|
204
|
+
_, motivo = broker._host_permitido(estado, "NOPE", "api.openai.com")
|
|
205
|
+
afirma("an unknown secret name is refused", motivo is not None)
|
|
206
|
+
|
|
207
|
+
quien = {"actor": "web", "repo": base}
|
|
208
|
+
code, resp = broker.accion_fetch(quien, {"secret": "OPENAI"}, estado)
|
|
209
|
+
comprueba("fetch without a host is 422", code, 422)
|
|
210
|
+
code, resp = broker.accion_fetch(
|
|
211
|
+
quien, {"secret": "OPENAI", "host": "evil.example.com", "path": "/v1/x"}, estado)
|
|
212
|
+
comprueba("fetch to an unbound host is 403", code, 403)
|
|
213
|
+
afirma("the 403 never echoes the secret value", "sk-REAL-SECRET" not in str(resp))
|
|
214
|
+
code, resp = broker.accion_fetch(
|
|
215
|
+
quien, {"secret": "OPENAI", "host": "api.openai.com", "method": "DELETE"}, estado)
|
|
216
|
+
comprueba("an unsupported method is 422", code, 422)
|
|
217
|
+
|
|
218
|
+
# The success path: capture the request without a socket, prove the
|
|
219
|
+
# secret is injected into the declared header only for the bound host.
|
|
220
|
+
capturado = {}
|
|
221
|
+
|
|
222
|
+
class _Resp:
|
|
223
|
+
status = 200
|
|
224
|
+
|
|
225
|
+
def __enter__(self):
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def __exit__(self, *a):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
def read(self, *_):
|
|
232
|
+
return b'{"ok":true}'
|
|
233
|
+
|
|
234
|
+
def _fake_open(req, timeout=0):
|
|
235
|
+
capturado["url"] = req.full_url
|
|
236
|
+
capturado["auth"] = req.headers.get("X-api-key") or req.headers.get("Authorization")
|
|
237
|
+
return _Resp()
|
|
238
|
+
|
|
239
|
+
# Patch the no-redirect opener the action actually uses.
|
|
240
|
+
original = broker._SIN_REDIRECCIONES.open
|
|
241
|
+
broker._SIN_REDIRECCIONES.open = _fake_open
|
|
242
|
+
try:
|
|
243
|
+
code, resp = broker.accion_fetch(
|
|
244
|
+
quien,
|
|
245
|
+
{"secret": "OPENAI", "host": "api.openai.com", "path": "/v1/models",
|
|
246
|
+
"header": "X-Api-Key", "template": "{secret}"},
|
|
247
|
+
estado)
|
|
248
|
+
# Control characters in a window-controlled field are refused as 422,
|
|
249
|
+
# not left to raise deep inside urllib as a 500.
|
|
250
|
+
code_ctl, _ = broker.accion_fetch(
|
|
251
|
+
quien,
|
|
252
|
+
{"secret": "OPENAI", "host": "api.openai.com", "path": "/x",
|
|
253
|
+
"header": "X-Api-Key", "template": "{secret}\r\nX-Injected: 1"},
|
|
254
|
+
estado)
|
|
255
|
+
finally:
|
|
256
|
+
broker._SIN_REDIRECCIONES.open = original
|
|
257
|
+
comprueba("a bound fetch returns 200", code, 200)
|
|
258
|
+
comprueba("it hit the exact bound host over https",
|
|
259
|
+
capturado["url"], "https://api.openai.com/v1/models")
|
|
260
|
+
comprueba("the secret was injected into the declared header",
|
|
261
|
+
capturado["auth"], "sk-REAL-SECRET")
|
|
262
|
+
afirma("and the broker's own response never carries the secret",
|
|
263
|
+
"sk-REAL-SECRET" not in str(resp))
|
|
264
|
+
comprueba("a CRLF-injected header is refused with 422", code_ctl, 422)
|
|
265
|
+
afirma("the redirect handler refuses to follow any 3xx (secret stays on host)",
|
|
266
|
+
broker._NoSigue().redirect_request(None, None, None, None, None) is None)
|
|
267
|
+
|
|
268
|
+
# Every audit line carries an evidence state, and the chain holds.
|
|
269
|
+
lineas = open(os.path.join(estado, "audit.log")).read().splitlines()
|
|
270
|
+
estados = {json.loads(x).get("evidence") for x in lineas}
|
|
271
|
+
afirma("audit lines carry evidence states",
|
|
272
|
+
str(Evidencia.IMPUESTO) in estados)
|
|
273
|
+
afirma("no audit line ever claims 'unknown means ok'",
|
|
274
|
+
all(not (json.loads(x).get("ok") and json.loads(x).get("evidence") ==
|
|
275
|
+
str(Evidencia.DESCONOCIDO)) for x in lineas))
|
|
276
|
+
comprueba("the chain verifies intact after secret ops",
|
|
277
|
+
broker.verifica(estado), 0)
|
|
278
|
+
finally:
|
|
279
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
280
|
+
os.environ.pop("AGENTS_CITY_HOME", None)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def arranca_sin_dns_inversa():
|
|
284
|
+
"""A loopback broker must start without waiting on the network.
|
|
285
|
+
|
|
286
|
+
The stdlib's HTTPServer.server_bind calls socket.getfqdn() for a cosmetic
|
|
287
|
+
name — a reverse DNS lookup. On a resolver that never answers (a CI macOS
|
|
288
|
+
runner, an airplane, a locked-down office LAN) it blocks for tens of
|
|
289
|
+
seconds, and the broker's endpoint appears only after it returns. This is
|
|
290
|
+
that hang's tombstone: getfqdn is sabotaged to take forever, and the
|
|
291
|
+
broker must still be serving.
|
|
292
|
+
"""
|
|
293
|
+
import socket
|
|
294
|
+
import threading
|
|
295
|
+
|
|
296
|
+
base = tempfile.mkdtemp(prefix="agents-city-broker-dns-")
|
|
297
|
+
os.environ["AGENTS_CITY_HOME"] = os.path.join(base, "app")
|
|
298
|
+
datos = os.path.join(base, "city")
|
|
299
|
+
os.makedirs(datos)
|
|
300
|
+
original = socket.getfqdn
|
|
301
|
+
socket.getfqdn = lambda *a: time.sleep(60) or "nunca"
|
|
302
|
+
servidor = None
|
|
303
|
+
try:
|
|
304
|
+
arrancado = threading.Event()
|
|
305
|
+
|
|
306
|
+
def sirve_en_hilo():
|
|
307
|
+
nonlocal servidor
|
|
308
|
+
servidor = broker.Servidor(("127.0.0.1", 0), broker.Manejador)
|
|
309
|
+
arrancado.set()
|
|
310
|
+
servidor.serve_forever()
|
|
311
|
+
|
|
312
|
+
broker.Manejador.estado = broker.estado_de(datos)
|
|
313
|
+
hilo = threading.Thread(target=sirve_en_hilo, daemon=True)
|
|
314
|
+
comienzo = time.monotonic()
|
|
315
|
+
hilo.start()
|
|
316
|
+
listo = arrancado.wait(timeout=5)
|
|
317
|
+
afirma(
|
|
318
|
+
"the broker binds without waiting on reverse DNS",
|
|
319
|
+
listo and time.monotonic() - comienzo < 5,
|
|
320
|
+
f"{round(time.monotonic() - comienzo, 2)}s",
|
|
321
|
+
)
|
|
322
|
+
finally:
|
|
323
|
+
socket.getfqdn = original
|
|
324
|
+
if servidor is not None:
|
|
325
|
+
servidor.shutdown()
|
|
326
|
+
servidor.server_close()
|
|
327
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
328
|
+
os.environ.pop("AGENTS_CITY_HOME", None)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
todo()
|
|
332
|
+
secretos_y_evidencia()
|
|
333
|
+
arranca_sin_dns_inversa()
|
|
334
|
+
sys.exit(resumen("broker"))
|