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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Configure the optional remote-road transport on this development machine.
|
|
3
|
+
# City identity is deliberately NOT written here: every running seat derives its
|
|
4
|
+
# own owner/city address from the selected city.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
BUS_URL="${CITY_BUS_URL:-https://bus.<your-subdomain>.workers.dev}"
|
|
9
|
+
CONF_DIR="$HOME/.claude/channels/city-bus"
|
|
10
|
+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
|
+
CHANNEL_DIR="$(cd "$REPO_DIR/../plugin/channel" && pwd)"
|
|
12
|
+
|
|
13
|
+
echo "── Configure remote city roads ─────────────────────────────────"
|
|
14
|
+
echo
|
|
15
|
+
|
|
16
|
+
if command -v bun >/dev/null 2>&1; then
|
|
17
|
+
RUNNER=bun
|
|
18
|
+
elif command -v node >/dev/null 2>&1; then
|
|
19
|
+
RUNNER=node
|
|
20
|
+
else
|
|
21
|
+
echo "I need node or bun on PATH." >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "→ installing the channel's development dependencies..."
|
|
26
|
+
(cd "$CHANNEL_DIR" && { command -v bun >/dev/null 2>&1 && bun install --silent || npm install --silent; })
|
|
27
|
+
|
|
28
|
+
read -rp "Owner prefix on the relay (for example alice): " OWNER
|
|
29
|
+
if ! [[ "$OWNER" =~ ^[a-z0-9][a-z0-9_-]{0,31}$ ]]; then
|
|
30
|
+
echo "Invalid owner: use lowercase letters, digits, dashes or underscores." >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
read -rsp "Paste the token (starts with rb_; it will not be shown): " TOKEN; echo
|
|
35
|
+
if ! [[ "$TOKEN" =~ ^rb_[a-f0-9]{48}$ ]]; then
|
|
36
|
+
echo "That token does not look right (rb_ followed by 48 hex characters)." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
mkdir -p "$CONF_DIR"
|
|
41
|
+
umask 077
|
|
42
|
+
cat > "$CONF_DIR/.env" <<EOF
|
|
43
|
+
CITY_BUS_URL=$BUS_URL
|
|
44
|
+
CITY_BUS_TOKEN=$TOKEN
|
|
45
|
+
EOF
|
|
46
|
+
chmod 600 "$CONF_DIR/.env"
|
|
47
|
+
echo "✓ transport settings written without a global city address"
|
|
48
|
+
|
|
49
|
+
echo "→ checking authentication with the temporary address $OWNER/setup-check..."
|
|
50
|
+
RESULT="$(cd "$CHANNEL_DIR" && CITY_BUS_URL="$BUS_URL" CITY_BUS_TOKEN="$TOKEN" \
|
|
51
|
+
CITY_BUS_AGENT="$OWNER/setup-check" timeout 20 "$RUNNER" --eval '
|
|
52
|
+
const u = new URL("/ws", process.env.CITY_BUS_URL)
|
|
53
|
+
u.searchParams.set("agent", process.env.CITY_BUS_AGENT)
|
|
54
|
+
u.protocol = u.protocol.replace(/^http/, "ws")
|
|
55
|
+
const WS = globalThis.WebSocket || (await import("ws")).default
|
|
56
|
+
const ws = new WS(u.toString(), { headers: { Authorization: "Bearer " + process.env.CITY_BUS_TOKEN } })
|
|
57
|
+
ws.addEventListener("message", e => { const m = JSON.parse(String(e.data))
|
|
58
|
+
if (m.type === "welcome") { console.log("OK"); process.exit(0) } })
|
|
59
|
+
ws.addEventListener("close", e => { console.log("FAIL " + e.code + " " + e.reason); process.exit(1) })
|
|
60
|
+
setTimeout(() => { console.log("FAIL timeout"); process.exit(1) }, 15000)
|
|
61
|
+
' 2>&1 | tail -1)"
|
|
62
|
+
|
|
63
|
+
if [ "$RESULT" = "OK" ]; then
|
|
64
|
+
echo "✓ the relay accepts this owner prefix"
|
|
65
|
+
else
|
|
66
|
+
echo "✗ I could not authenticate: $RESULT" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
cat <<'EOF'
|
|
71
|
+
|
|
72
|
+
Done. Open a road by exchanging invitations, then start each city seat:
|
|
73
|
+
|
|
74
|
+
agents-city road invite <city>
|
|
75
|
+
agents-city road connect <city> <other.invitation.json>
|
|
76
|
+
agents-city seat --city <city>
|
|
77
|
+
|
|
78
|
+
The seat derives owner/city itself. Do not add CITY_BUS_AGENT to the shared .env.
|
|
79
|
+
EOF
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Compatibility entrypoint for the canonical local-channel integration test. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const test = fileURLToPath(new URL('../../bin/test-channel.py', import.meta.url));
|
|
6
|
+
const result = spawnSync(test, { stdio: 'inherit' });
|
|
7
|
+
|
|
8
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { request as httpRequest } from 'node:http';
|
|
2
|
+
|
|
3
|
+
const BASE = 'http://127.0.0.1:8799';
|
|
4
|
+
const ALICE = 'tok-alice-test';
|
|
5
|
+
const BOB = 'tok-bob-test';
|
|
6
|
+
|
|
7
|
+
function open(token: string, agent: string): Promise<{ ws: WebSocket; msgs: any[] }> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const msgs: any[] = [];
|
|
10
|
+
const ws = new WebSocket(`${BASE.replace('http', 'ws')}/ws?agent=${agent}`, {
|
|
11
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
12
|
+
} as any);
|
|
13
|
+
ws.addEventListener('message', (e) => {
|
|
14
|
+
try {
|
|
15
|
+
msgs.push(JSON.parse(String(e.data)));
|
|
16
|
+
} catch {}
|
|
17
|
+
});
|
|
18
|
+
ws.addEventListener('open', () => resolve({ ws, msgs }));
|
|
19
|
+
ws.addEventListener('error', () => reject(new Error('connection error')));
|
|
20
|
+
ws.addEventListener('close', (e) => reject(new Error(`closed ${e.code} ${e.reason}`)));
|
|
21
|
+
setTimeout(() => reject(new Error('timeout')), 5000);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
function rejected(path: string, token = ''): Promise<{ status: number; body: string }> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const url = new URL(path, BASE);
|
|
28
|
+
const req = httpRequest(url, {
|
|
29
|
+
headers: {
|
|
30
|
+
Connection: 'Upgrade',
|
|
31
|
+
Upgrade: 'websocket',
|
|
32
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
req.on('response', (response) => {
|
|
36
|
+
let body = '';
|
|
37
|
+
response.setEncoding('utf8');
|
|
38
|
+
response.on('data', (chunk) => {
|
|
39
|
+
body += chunk;
|
|
40
|
+
});
|
|
41
|
+
response.on('end', () => resolve({ status: response.statusCode || 0, body }));
|
|
42
|
+
});
|
|
43
|
+
req.on('upgrade', (_response, socket) => {
|
|
44
|
+
socket.destroy();
|
|
45
|
+
resolve({ status: 101, body: '' });
|
|
46
|
+
});
|
|
47
|
+
req.on('error', reject);
|
|
48
|
+
req.end();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const roadEnvelope = (from: string, to: string, text: string) => ({
|
|
52
|
+
protocol: 'agents-city-bus/2',
|
|
53
|
+
id: `msg_${crypto.randomUUID()}`,
|
|
54
|
+
kind: 'road.message',
|
|
55
|
+
scope: 'road',
|
|
56
|
+
thread: null,
|
|
57
|
+
from: { city: from, actor: 'seat', role: 'chair' },
|
|
58
|
+
to: { city: to, actor: 'seat' },
|
|
59
|
+
createdAt: new Date().toISOString(),
|
|
60
|
+
payload: { text, trust: 'information-not-authority' },
|
|
61
|
+
});
|
|
62
|
+
let pass = 0,
|
|
63
|
+
fail = 0;
|
|
64
|
+
function check(name: string, ok: boolean, extra = '') {
|
|
65
|
+
console.log(`${ok ? '✅' : '❌'} ${name}${extra ? ' — ' + extra : ''}`);
|
|
66
|
+
ok ? pass++ : fail++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- HTTP: rechazos ---
|
|
70
|
+
const noTok = await rejected('/ws');
|
|
71
|
+
check('no token -> 401', noTok.status === 401, `status ${noTok.status}`);
|
|
72
|
+
|
|
73
|
+
const badTok = await rejected('/ws?agent=alice/lead', 'made-up');
|
|
74
|
+
check('invalid token -> 403', badTok.status === 403, `status ${badTok.status}`);
|
|
75
|
+
|
|
76
|
+
const badName = await rejected('/ws?agent=MAYUS', ALICE);
|
|
77
|
+
check('invalid name -> 400', badName.status === 400, `status ${badName.status}`);
|
|
78
|
+
|
|
79
|
+
const imperson = await rejected('/ws?agent=bob/infra', ALICE);
|
|
80
|
+
check(
|
|
81
|
+
'impersonating another user -> 403',
|
|
82
|
+
imperson.status === 403,
|
|
83
|
+
`status ${imperson.status} ${imperson.body.trim()}`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// --- WS: flujo real ---
|
|
87
|
+
const a = await open(ALICE, 'alice/lead');
|
|
88
|
+
const b = await open(BOB, 'bob/infra');
|
|
89
|
+
await wait(400);
|
|
90
|
+
|
|
91
|
+
check(
|
|
92
|
+
'a welcome on connecting',
|
|
93
|
+
a.msgs.some((m) => m.type === 'welcome'),
|
|
94
|
+
);
|
|
95
|
+
check(
|
|
96
|
+
'the second agent shows up',
|
|
97
|
+
a.msgs.some((m) => m.type === 'presence' && m.agent === 'bob/infra' && m.status === 'online'),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// roster
|
|
101
|
+
b.ws.send(JSON.stringify({ type: 'roster' }));
|
|
102
|
+
await wait(300);
|
|
103
|
+
const ros = b.msgs.filter((m) => m.type === 'roster').pop();
|
|
104
|
+
check(
|
|
105
|
+
'the roster sees the other one',
|
|
106
|
+
!!ros?.agents?.some((x: any) => x.agent === 'alice/lead'),
|
|
107
|
+
JSON.stringify(ros?.agents),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// mensaje dirigido
|
|
111
|
+
a.ws.send(JSON.stringify({ type: 'send', to: 'bob/infra', text: 'hello from the seat' }));
|
|
112
|
+
await wait(400);
|
|
113
|
+
const got = b.msgs.find((m) => m.type === 'msg');
|
|
114
|
+
check(
|
|
115
|
+
'the message is delivered',
|
|
116
|
+
got?.text === 'hello from the seat' && got?.from === 'alice/lead',
|
|
117
|
+
JSON.stringify(got),
|
|
118
|
+
);
|
|
119
|
+
check(
|
|
120
|
+
'the sender gets an acknowledgement',
|
|
121
|
+
a.msgs.some((m) => m.type === 'sent' && m.delivered === 1),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// The v2 local hub and this internet relay carry the same typed envelope.
|
|
125
|
+
const typed = roadEnvelope('alice/lead', 'bob/infra', 'typed over the road');
|
|
126
|
+
a.ws.send(
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
type: 'send',
|
|
129
|
+
request_id: 'typed-1',
|
|
130
|
+
to: 'bob/infra',
|
|
131
|
+
text: 'typed over the road',
|
|
132
|
+
envelope: typed,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
await wait(400);
|
|
136
|
+
const typedGot = b.msgs.find((m) => m.type === 'msg' && m.envelope?.id === typed.id);
|
|
137
|
+
check(
|
|
138
|
+
'the relay preserves the v2 envelope',
|
|
139
|
+
typedGot?.envelope?.protocol === 'agents-city-bus/2' &&
|
|
140
|
+
typedGot?.envelope?.scope === 'road' &&
|
|
141
|
+
typedGot?.envelope?.from?.actor === 'seat' &&
|
|
142
|
+
typedGot?.envelope?.from?.role === 'external-seat',
|
|
143
|
+
);
|
|
144
|
+
check(
|
|
145
|
+
'typed acknowledgements correlate to their request',
|
|
146
|
+
a.msgs.some((m) => m.type === 'sent' && m.request_id === 'typed-1'),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const internal = { ...roadEnvelope('alice/lead', 'bob/infra', 'must fail'), scope: 'internal' };
|
|
150
|
+
a.ws.send(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
type: 'send',
|
|
153
|
+
request_id: 'typed-2',
|
|
154
|
+
to: 'bob/infra',
|
|
155
|
+
text: 'must fail',
|
|
156
|
+
envelope: internal,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
await wait(300);
|
|
160
|
+
check(
|
|
161
|
+
'an internal committee envelope cannot cross the internet boundary',
|
|
162
|
+
a.msgs.some((m) => m.type === 'error' && m.request_id === 'typed-2' && /scope/.test(m.error)),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const spoofed = roadEnvelope('alice/other', 'bob/infra', 'must fail too');
|
|
166
|
+
a.ws.send(
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
type: 'send',
|
|
169
|
+
request_id: 'typed-3',
|
|
170
|
+
to: 'bob/infra',
|
|
171
|
+
text: 'must fail too',
|
|
172
|
+
envelope: spoofed,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
await wait(300);
|
|
176
|
+
check(
|
|
177
|
+
'the authenticated seat cannot spoof another city in an envelope',
|
|
178
|
+
a.msgs.some((m) => m.type === 'error' && m.request_id === 'typed-3' && /sender/.test(m.error)),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// respuesta en sentido contrario
|
|
182
|
+
b.ws.send(JSON.stringify({ type: 'send', to: 'alice/lead', text: 'recibido' }));
|
|
183
|
+
await wait(400);
|
|
184
|
+
check(
|
|
185
|
+
'a reply travels the other way',
|
|
186
|
+
a.msgs.some((m) => m.type === 'msg' && m.text === 'recibido' && m.from === 'bob/infra'),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// destinatario inexistente: ahora se encola en vez de fallar
|
|
190
|
+
a.ws.send(JSON.stringify({ type: 'send', to: 'nadie/x', text: 'eco' }));
|
|
191
|
+
await wait(400);
|
|
192
|
+
check(
|
|
193
|
+
'recipient offline -> queued',
|
|
194
|
+
a.msgs.some((m) => m.type === 'queued' && m.to === 'nadie/x'),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// a uno mismo
|
|
198
|
+
a.ws.send(JSON.stringify({ type: 'send', to: 'alice/lead', text: 'yo' }));
|
|
199
|
+
await wait(300);
|
|
200
|
+
check(
|
|
201
|
+
'message to yourself -> error',
|
|
202
|
+
a.msgs.some((m) => m.type === 'error' && /that is you/.test(m.error)),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// broadcast
|
|
206
|
+
// A second agent of the SAME person: that is the normal case — a seat window
|
|
207
|
+
// and a repo window. The name here used to be left over from a find-and-replace
|
|
208
|
+
// and carried a space, which the hub rightly refuses, so this test never ran.
|
|
209
|
+
const c = await open(ALICE, 'alice/second');
|
|
210
|
+
await wait(300);
|
|
211
|
+
const bBefore = b.msgs.filter((m) => m.type === 'msg').length;
|
|
212
|
+
const cBefore = c.msgs.filter((m) => m.type === 'msg').length;
|
|
213
|
+
a.ws.send(JSON.stringify({ type: 'send', to: '*', text: 'to everybody' }));
|
|
214
|
+
await wait(500);
|
|
215
|
+
check(
|
|
216
|
+
'a broadcast reaches everybody else',
|
|
217
|
+
b.msgs.filter((m) => m.type === 'msg').length === bBefore + 1 &&
|
|
218
|
+
c.msgs.filter((m) => m.type === 'msg').length === cBefore + 1,
|
|
219
|
+
);
|
|
220
|
+
check(
|
|
221
|
+
'a broadcast does not come back to the sender',
|
|
222
|
+
!a.msgs.some((m) => m.type === 'msg' && m.text === 'to everybody'),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// size
|
|
226
|
+
a.ws.send(JSON.stringify({ type: 'send', to: 'bob/infra', text: 'x'.repeat(64_001) }));
|
|
227
|
+
await wait(400);
|
|
228
|
+
check(
|
|
229
|
+
'an enormous message is refused',
|
|
230
|
+
a.msgs.some((m) => m.type === 'error' && /maximum/.test(m.error)),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// empty
|
|
234
|
+
a.ws.send(JSON.stringify({ type: 'send', to: 'bob/infra', text: ' ' }));
|
|
235
|
+
await wait(300);
|
|
236
|
+
check(
|
|
237
|
+
'an empty message is refused',
|
|
238
|
+
a.msgs.some((m) => m.type === 'error' && /empty/.test(m.error)),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// the same agent reconnecting closes the previous session
|
|
242
|
+
let closed = 0;
|
|
243
|
+
b.ws.addEventListener('close', (e) => {
|
|
244
|
+
if (e.code === 4009) closed = 4009;
|
|
245
|
+
});
|
|
246
|
+
const b2 = await open(BOB, 'bob/infra');
|
|
247
|
+
await wait(600);
|
|
248
|
+
check('a reconnect closes the previous session', closed === 4009, `code=${closed}`);
|
|
249
|
+
|
|
250
|
+
// rate limit
|
|
251
|
+
const d = await open(BOB, 'bob/tmp'); // the token is bob's, so the name has to be too
|
|
252
|
+
await wait(200);
|
|
253
|
+
for (let i = 0; i < 35; i++)
|
|
254
|
+
d.ws.send(JSON.stringify({ type: 'send', to: 'alice/lead', text: `n${i}` }));
|
|
255
|
+
await wait(900);
|
|
256
|
+
check(
|
|
257
|
+
'the rate limit fires',
|
|
258
|
+
d.msgs.some((m) => m.type === 'error' && /rate limit/.test(m.error)),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
for (const s of [a.ws, b2.ws, c.ws, d.ws])
|
|
262
|
+
try {
|
|
263
|
+
s.close();
|
|
264
|
+
} catch {}
|
|
265
|
+
console.log(`\n${pass} ok, ${fail} failed`);
|
|
266
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Exercise the internet-road Durable Object locally, including its durable queue.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
WORKER="$(cd "$(dirname "$0")/../worker" && pwd)"
|
|
6
|
+
STATE="$(mktemp -d /tmp/agents-city-bus.XXXXXX)"
|
|
7
|
+
LOG="$STATE/wrangler.log"
|
|
8
|
+
PID=""
|
|
9
|
+
cleanup() {
|
|
10
|
+
if [ -n "$PID" ]; then
|
|
11
|
+
kill "$PID" 2>/dev/null || true
|
|
12
|
+
wait "$PID" 2>/dev/null || true
|
|
13
|
+
fi
|
|
14
|
+
rm -rf "$STATE"
|
|
15
|
+
}
|
|
16
|
+
trap cleanup EXIT INT TERM
|
|
17
|
+
|
|
18
|
+
cd "$WORKER"
|
|
19
|
+
alice_hash="$(printf '%s' 'tok-alice-test' | shasum -a 256 | awk '{print $1}')"
|
|
20
|
+
bob_hash="$(printf '%s' 'tok-bob-test' | shasum -a 256 | awk '{print $1}')"
|
|
21
|
+
npm exec wrangler -- kv key put --local --persist-to "$STATE" --binding TOKENS "tok:$alice_hash" '{"user":"alice","can_send_to":["*"]}' >/dev/null
|
|
22
|
+
npm exec wrangler -- kv key put --local --persist-to "$STATE" --binding TOKENS "tok:$bob_hash" '{"user":"bob","can_send_to":["*"]}' >/dev/null
|
|
23
|
+
npm exec wrangler -- dev --local --persist-to "$STATE" --port 8799 --log-level error --show-interactive-dev-session=false >"$LOG" 2>&1 &
|
|
24
|
+
PID=$!
|
|
25
|
+
|
|
26
|
+
ready=0
|
|
27
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
28
|
+
if curl -fsS http://127.0.0.1:8799/health >/dev/null 2>&1; then
|
|
29
|
+
ready=1
|
|
30
|
+
break
|
|
31
|
+
fi
|
|
32
|
+
sleep .25
|
|
33
|
+
done
|
|
34
|
+
if [ "$ready" -ne 1 ]; then
|
|
35
|
+
cat "$LOG" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
NODE_NO_WARNINGS=1 node --experimental-strip-types ../scripts/test-hub.ts
|
|
40
|
+
NODE_NO_WARNINGS=1 node --experimental-strip-types ../scripts/test-queue.ts
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const BASE = process.env.BUS || 'http://127.0.0.1:8799';
|
|
2
|
+
const ALICE = process.env.TJ || 'tok-alice-test';
|
|
3
|
+
const BOB = process.env.TA || 'tok-bob-test';
|
|
4
|
+
// The prefixes have to match the owners of those tokens.
|
|
5
|
+
const UA = process.env.UA || 'alice'; // owner of TA
|
|
6
|
+
const UB = process.env.UB || 'bob'; // owner of TB
|
|
7
|
+
function open(token: string, agent: string): Promise<{ ws: WebSocket; msgs: any[] }> {
|
|
8
|
+
return new Promise((res, rej) => {
|
|
9
|
+
const msgs: any[] = [];
|
|
10
|
+
const ws = new WebSocket(`${BASE.replace('http', 'ws')}/ws?agent=${agent}`, {
|
|
11
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
12
|
+
} as any);
|
|
13
|
+
ws.addEventListener('message', (e) => {
|
|
14
|
+
try {
|
|
15
|
+
msgs.push(JSON.parse(String(e.data)));
|
|
16
|
+
} catch {}
|
|
17
|
+
});
|
|
18
|
+
ws.addEventListener('open', () => res({ ws, msgs }));
|
|
19
|
+
ws.addEventListener('close', (e) => rej(new Error('cerrado ' + e.code)));
|
|
20
|
+
setTimeout(() => rej(new Error('timeout')), 8000);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const wait = (m: number) => new Promise((r) => setTimeout(r, m));
|
|
24
|
+
let p = 0,
|
|
25
|
+
f = 0;
|
|
26
|
+
const ck = (n: string, ok: boolean, x = '') => {
|
|
27
|
+
console.log((ok ? '✅' : '❌') + ' ' + n + (x ? ' — ' + x : ''));
|
|
28
|
+
ok ? p++ : f++;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 1. write to somebody who is NOT connected
|
|
32
|
+
const a = await open(ALICE, `${UA}/cpto`);
|
|
33
|
+
await wait(400);
|
|
34
|
+
a.ws.send(
|
|
35
|
+
JSON.stringify({ type: 'send', to: `${UB}/away`, text: 'revisa los CVE de las dependencias' }),
|
|
36
|
+
);
|
|
37
|
+
await wait(600);
|
|
38
|
+
const q = a.msgs.find((m: any) => m.type === 'queued');
|
|
39
|
+
ck('recipient away -> queued, not an error', !!q && q.to === `${UB}/away`, JSON.stringify(q));
|
|
40
|
+
|
|
41
|
+
// 2. un segundo mensaje para el mismo
|
|
42
|
+
a.ws.send(JSON.stringify({ type: 'send', to: `${UB}/away`, text: 'y el bundle size de la home' }));
|
|
43
|
+
await wait(600);
|
|
44
|
+
ck('a second message is queued too', a.msgs.filter((m: any) => m.type === 'queued').length === 2);
|
|
45
|
+
|
|
46
|
+
const typed = {
|
|
47
|
+
protocol: 'agents-city-bus/2',
|
|
48
|
+
id: `msg_${crypto.randomUUID()}`,
|
|
49
|
+
kind: 'road.message',
|
|
50
|
+
scope: 'road',
|
|
51
|
+
thread: null,
|
|
52
|
+
from: { city: `${UA}/cpto`, actor: 'seat', role: 'chair' },
|
|
53
|
+
to: { city: `${UB}/away`, actor: 'seat' },
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
payload: { text: 'typed and durable' },
|
|
56
|
+
};
|
|
57
|
+
a.ws.send(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
type: 'send',
|
|
60
|
+
to: `${UB}/away`,
|
|
61
|
+
text: 'typed and durable',
|
|
62
|
+
envelope: typed,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
await wait(600);
|
|
66
|
+
ck('a typed envelope is queued too', a.msgs.filter((m: any) => m.type === 'queued').length === 3);
|
|
67
|
+
|
|
68
|
+
// 3. el destinatario aparece y recibe lo pendiente, en orden
|
|
69
|
+
const b = await open(BOB, `${UB}/away`);
|
|
70
|
+
await wait(1200);
|
|
71
|
+
const got = b.msgs.filter((m: any) => m.type === 'msg');
|
|
72
|
+
ck('on connecting it gets all queued messages', got.length === 3, `received ${got.length}`);
|
|
73
|
+
ck(
|
|
74
|
+
'they arrive in order',
|
|
75
|
+
got[0]?.text === 'revisa los CVE de las dependencias' &&
|
|
76
|
+
got[1]?.text === 'y el bundle size de la home',
|
|
77
|
+
);
|
|
78
|
+
ck(
|
|
79
|
+
'marked as delayed',
|
|
80
|
+
typeof got[0]?.delayed_minutes === 'number',
|
|
81
|
+
`delayed_minutes=${got[0]?.delayed_minutes}`,
|
|
82
|
+
);
|
|
83
|
+
ck('they keep their sender', got[0]?.from === `${UA}/cpto`);
|
|
84
|
+
ck(
|
|
85
|
+
'the typed envelope survives durable storage',
|
|
86
|
+
got[2]?.envelope?.id === typed.id && got[2]?.envelope?.from?.role === 'external-seat',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// 4. the queue drains: reconnecting does not duplicate them
|
|
90
|
+
b.ws.close();
|
|
91
|
+
await wait(600);
|
|
92
|
+
const b2 = await open(BOB, `${UB}/away`);
|
|
93
|
+
await wait(1000);
|
|
94
|
+
ck(
|
|
95
|
+
'they are not delivered twice on reconnect',
|
|
96
|
+
b2.msgs.filter((m: any) => m.type === 'msg').length === 0,
|
|
97
|
+
`received ${b2.msgs.filter((m: any) => m.type === 'msg').length}`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// 5. estando conectado, entrega directa (no cola)
|
|
101
|
+
a.ws.send(JSON.stringify({ type: 'send', to: `${UB}/away`, text: 'directo' }));
|
|
102
|
+
await wait(600);
|
|
103
|
+
ck(
|
|
104
|
+
'with the peer connected it goes straight through',
|
|
105
|
+
a.msgs.some((m: any) => m.type === 'sent' && m.delivered === 1),
|
|
106
|
+
);
|
|
107
|
+
ck(
|
|
108
|
+
'and arrives with no delay marker',
|
|
109
|
+
b2.msgs.some(
|
|
110
|
+
(m: any) => m.type === 'msg' && m.text === 'directo' && m.delayed_minutes === undefined,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// 6. a broadcast with nobody else does not queue
|
|
115
|
+
try {
|
|
116
|
+
const solo = await open(ALICE, `${UA}/solo`);
|
|
117
|
+
await wait(400);
|
|
118
|
+
solo.ws.send(JSON.stringify({ type: 'send', to: '*', text: 'eco' }));
|
|
119
|
+
await wait(600);
|
|
120
|
+
ck(
|
|
121
|
+
'a broadcast reaches whoever is connected',
|
|
122
|
+
solo.msgs.some((m: any) => m.type === 'sent'),
|
|
123
|
+
);
|
|
124
|
+
solo.ws.close();
|
|
125
|
+
} catch (e: any) {
|
|
126
|
+
ck('broadcast', false, e.message);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
a.ws.close();
|
|
130
|
+
b2.ws.close();
|
|
131
|
+
console.log(`\n${p} ok, ${f} failed`);
|
|
132
|
+
process.exit(f ? 1 : 0);
|