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,488 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Codex opens its official remote TUI and the bus safely joins its thread."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
RAIZ = os.path.dirname(AQUI)
|
|
14
|
+
sys.path.insert(0, AQUI)
|
|
15
|
+
from testlib import afirma, comprueba, detiene_hubs_de_ciudad, detiene_proceso, resumen # noqa: E402
|
|
16
|
+
|
|
17
|
+
GATEWAY = os.path.join(RAIZ, 'plugin', 'channel', 'runtime-gateway.js')
|
|
18
|
+
CLIENT = os.path.join(RAIZ, 'plugin', 'channel', 'client.js')
|
|
19
|
+
FAKE = os.path.join(RAIZ, 'benchmarks', 'latency', 'fake-native-server.mjs')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def puerto():
|
|
23
|
+
with socket.socket() as sock:
|
|
24
|
+
sock.bind(('127.0.0.1', 0))
|
|
25
|
+
return sock.getsockname()[1]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def espera(condicion, segundos=10):
|
|
29
|
+
limite = time.monotonic() + segundos
|
|
30
|
+
while time.monotonic() < limite:
|
|
31
|
+
if condicion():
|
|
32
|
+
return True
|
|
33
|
+
time.sleep(.05)
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def lee(path):
|
|
38
|
+
if not os.path.exists(path):
|
|
39
|
+
return ''
|
|
40
|
+
with open(path, encoding='utf-8', errors='replace') as stream:
|
|
41
|
+
return stream.read()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def lineas_json(path):
|
|
45
|
+
if not os.path.exists(path):
|
|
46
|
+
return []
|
|
47
|
+
with open(path, encoding='utf-8') as stream:
|
|
48
|
+
return [json.loads(line) for line in stream if line.strip()]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def solicitudes(path):
|
|
52
|
+
return [row for row in lineas_json(path) if row.get('request')]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def aprobaciones(path):
|
|
56
|
+
return [row for row in lineas_json(path) if row.get('approval')]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
print('\n Codex native TUI over its city WebSocket thread')
|
|
61
|
+
if not shutil.which('node'):
|
|
62
|
+
afirma('· node is available', False, 'node missing')
|
|
63
|
+
return resumen('runtime-ui')
|
|
64
|
+
|
|
65
|
+
base = tempfile.mkdtemp(prefix='agents-city-runtime-ui-')
|
|
66
|
+
app = os.path.join(base, 'app')
|
|
67
|
+
city = os.path.join(app, 'alice', 'home')
|
|
68
|
+
repo = os.path.join(base, 'repos', 'codex-agent')
|
|
69
|
+
fake_bin = os.path.join(base, 'bin')
|
|
70
|
+
for path in (city, repo, fake_bin):
|
|
71
|
+
os.makedirs(path)
|
|
72
|
+
open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
|
|
73
|
+
'id: city_native_ui\nname: Home\nslug: home\nowner: alice\n')
|
|
74
|
+
open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
|
|
75
|
+
'{"version": 1, "roads": []}\n')
|
|
76
|
+
open(os.path.join(city, 'alice.md'), 'w', encoding='utf-8').write(
|
|
77
|
+
'---\nuser: alice\nagent: alice/ceo\nrepos: [codex-agent]\n'
|
|
78
|
+
'role.codex-agent: engineer\nruns.codex-agent: codex\n---\n')
|
|
79
|
+
|
|
80
|
+
tui_args = os.path.join(base, 'tui-args.txt')
|
|
81
|
+
tui_pid = os.path.join(base, 'tui.pid')
|
|
82
|
+
tui_cwd = os.path.join(base, 'tui-cwd.txt')
|
|
83
|
+
fake_codex = os.path.join(fake_bin, 'codex')
|
|
84
|
+
open(fake_codex, 'w', encoding='utf-8').write(
|
|
85
|
+
f'#!{sys.executable}\n'
|
|
86
|
+
'import os, signal, sys, time\n'
|
|
87
|
+
'open(os.environ["CITY_TEST_TUI_ARGS"], "w").write("\\n".join(sys.argv[1:]) + "\\n")\n'
|
|
88
|
+
'open(os.environ["CITY_TEST_TUI_PID"], "w").write(str(os.getpid()) + "\\n")\n'
|
|
89
|
+
'open(os.environ["CITY_TEST_TUI_CWD"], "w").write(os.getcwd() + "\\n")\n'
|
|
90
|
+
'print("FAKE_CODEX_TUI_ATTACHED", flush=True)\n'
|
|
91
|
+
'def stop(_signal, _frame): raise SystemExit(0)\n'
|
|
92
|
+
'signal.signal(signal.SIGINT, stop)\n'
|
|
93
|
+
'signal.signal(signal.SIGTERM, stop)\n'
|
|
94
|
+
'signal.signal(signal.SIGHUP, stop)\n'
|
|
95
|
+
'while True: time.sleep(1)\n')
|
|
96
|
+
os.chmod(fake_codex, 0o755)
|
|
97
|
+
|
|
98
|
+
port = puerto()
|
|
99
|
+
url = f'ws://127.0.0.1:{port}'
|
|
100
|
+
capture = os.path.join(base, 'codex.jsonl')
|
|
101
|
+
behavior = os.path.join(base, 'behavior.txt')
|
|
102
|
+
open(behavior, 'w', encoding='utf-8').write('healthy\n')
|
|
103
|
+
log_path = os.path.join(base, 'gateway.log')
|
|
104
|
+
runtime_dir = os.path.join(app, '.runtime', 'bus', 'city-native-ui')
|
|
105
|
+
status_path = os.path.join(runtime_dir, 'gateways', 'codex-agent.json')
|
|
106
|
+
activity_path = os.path.join(runtime_dir, 'activity.jsonl')
|
|
107
|
+
diagnostic_path = os.path.join(runtime_dir, 'diagnostics.jsonl')
|
|
108
|
+
env = dict(
|
|
109
|
+
os.environ,
|
|
110
|
+
AGENTS_CITY_HOME=app,
|
|
111
|
+
AGENTS_CITY_DATA=city,
|
|
112
|
+
AGENTS_CITY_USER='alice',
|
|
113
|
+
CITY_CODEX_APP_SERVER_URL=url,
|
|
114
|
+
CITY_TEST_TUI_ARGS=tui_args,
|
|
115
|
+
CITY_TEST_TUI_PID=tui_pid,
|
|
116
|
+
CITY_TEST_TUI_CWD=tui_cwd,
|
|
117
|
+
FAKE_CODEX_TUI_CWD=repo,
|
|
118
|
+
# Compatibility case for an explicitly outer-caged gateway. The normal
|
|
119
|
+
# launcher no longer wraps Codex because MCP workers also sandbox, but
|
|
120
|
+
# the connector must still avoid nesting its turn sandbox if embedded.
|
|
121
|
+
CITY_OUTER_CAGE='1',
|
|
122
|
+
)
|
|
123
|
+
for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
|
|
124
|
+
env.pop(key, None)
|
|
125
|
+
|
|
126
|
+
server = None
|
|
127
|
+
gateway = None
|
|
128
|
+
stream = None
|
|
129
|
+
try:
|
|
130
|
+
server = subprocess.Popen(
|
|
131
|
+
['node', FAKE, 'codex', str(port), capture, behavior],
|
|
132
|
+
env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
133
|
+
ready = server.stdout.readline() if server.stdout else ''
|
|
134
|
+
afirma('· deterministic Codex app-server starts', '"ready":true' in ready, ready)
|
|
135
|
+
|
|
136
|
+
stream = open(log_path, 'w+', encoding='utf-8')
|
|
137
|
+
gateway = subprocess.Popen(
|
|
138
|
+
['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
|
|
139
|
+
'--cwd', repo, '--command', fake_codex, '--auto', '1', '--interactive', '1'],
|
|
140
|
+
env=env, stdin=subprocess.DEVNULL, stdout=stream, stderr=stream, text=True)
|
|
141
|
+
afirma('· interactive gateway authenticates and opens a child TUI',
|
|
142
|
+
espera(lambda: os.path.exists(status_path)
|
|
143
|
+
and os.path.exists(tui_args) and os.path.exists(tui_pid)),
|
|
144
|
+
lee(log_path))
|
|
145
|
+
|
|
146
|
+
arguments = lee(tui_args).splitlines()
|
|
147
|
+
comprueba('· it uses the official top-level remote TUI command',
|
|
148
|
+
arguments, ['--remote', url])
|
|
149
|
+
comprueba('· the TUI inherits the repo working directory',
|
|
150
|
+
os.path.realpath(lee(tui_cwd).strip()), os.path.realpath(repo))
|
|
151
|
+
log = lee(log_path)
|
|
152
|
+
afirma('· Codex UI replaces the custom city prompt',
|
|
153
|
+
'FAKE_CODEX_TUI_ATTACHED' in log and 'city> ' not in log, log)
|
|
154
|
+
afirma('· an empty TUI thread is adopted before its rollout exists',
|
|
155
|
+
'adopted over WebSocket; awaiting its first rollout' in log, log)
|
|
156
|
+
afirma('· happy: a normal TUI question and answer reach City live without a committee',
|
|
157
|
+
espera(lambda: any(
|
|
158
|
+
row.get('kind') == 'conversation.agent'
|
|
159
|
+
and row.get('summary') == 'This city coordinates its repo agents.'
|
|
160
|
+
for row in lineas_json(activity_path)))
|
|
161
|
+
and any(row.get('kind') == 'conversation.user'
|
|
162
|
+
and row.get('summary') == 'What is this city for?'
|
|
163
|
+
for row in lineas_json(activity_path)),
|
|
164
|
+
lee(activity_path))
|
|
165
|
+
diagnostics = lineas_json(diagnostic_path)
|
|
166
|
+
afirma('· the empty TUI adoption and later subscription are durable diagnostics',
|
|
167
|
+
any(row.get('event') == 'codex.thread.adopted' for row in diagnostics)
|
|
168
|
+
and any(row.get('event') == 'codex.thread.joined'
|
|
169
|
+
and row.get('replayedItems') == 2 for row in diagnostics),
|
|
170
|
+
lee(diagnostic_path))
|
|
171
|
+
afirma('· non-happy: private TUI reasoning is never mirrored',
|
|
172
|
+
'PRIVATE_TUI_REASONING' not in lee(activity_path), lee(activity_path))
|
|
173
|
+
|
|
174
|
+
open(behavior, 'w', encoding='utf-8').write('permission-request\n')
|
|
175
|
+
opened = subprocess.run(
|
|
176
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
177
|
+
input=json.dumps({
|
|
178
|
+
'question': 'Prove the attached Codex thread still receives bus work.',
|
|
179
|
+
'desiredOutcome': 'One native WebSocket acceptance.',
|
|
180
|
+
'definitionOfDone': ['the provider accepts exactly once'],
|
|
181
|
+
'participants': ['codex-agent'],
|
|
182
|
+
}), capture_output=True, text=True,
|
|
183
|
+
env=dict(env, CITY_BUS_ACTOR='seat'), timeout=12)
|
|
184
|
+
state = json.loads(opened.stdout) if opened.returncode == 0 else {}
|
|
185
|
+
afirma('· a real city committee assignment reaches the attached thread',
|
|
186
|
+
bool(state.get('id')) and espera(
|
|
187
|
+
lambda: len(solicitudes(capture)) == 1
|
|
188
|
+
and len(aprobaciones(capture)) == 1),
|
|
189
|
+
opened.stderr or lee(log_path))
|
|
190
|
+
delivered = solicitudes(capture)
|
|
191
|
+
approved = aprobaciones(capture)
|
|
192
|
+
afirma('· the materialized first TUI turn completes the real subscription',
|
|
193
|
+
'Codex TUI thread thread_tui joined over WebSocket' in lee(log_path),
|
|
194
|
+
lee(log_path))
|
|
195
|
+
afirma('· the gateway joins and delivers to the TUI-created thread',
|
|
196
|
+
len(delivered) == 1
|
|
197
|
+
and delivered[0].get('request', {}).get('params', {}).get('threadId')
|
|
198
|
+
== 'thread_tui',
|
|
199
|
+
lee(log_path))
|
|
200
|
+
first_params = delivered[0].get('request', {}).get('params', {})
|
|
201
|
+
afirma('· an explicitly outer-caged gateway avoids a second turn sandbox',
|
|
202
|
+
first_params.get('sandboxPolicy') == {'type': 'dangerFullAccess'}
|
|
203
|
+
and first_params.get('approvalPolicy') == 'on-request',
|
|
204
|
+
json.dumps(first_params))
|
|
205
|
+
first_approval = approved[0].get('approval', {}).get('result', {})
|
|
206
|
+
afirma('· auto mode approves the exact provider permissions for this turn',
|
|
207
|
+
first_approval.get('scope') == 'turn'
|
|
208
|
+
and first_approval.get('permissions', {}).get('network', {}).get('enabled') is True
|
|
209
|
+
and repo in first_approval.get('permissions', {}).get(
|
|
210
|
+
'fileSystem', {}).get('write', []),
|
|
211
|
+
json.dumps(first_approval))
|
|
212
|
+
time.sleep(.1)
|
|
213
|
+
log = lee(log_path)
|
|
214
|
+
afirma('· the gateway does not print duplicate model deltas over the TUI',
|
|
215
|
+
'fake codex response' not in log, log)
|
|
216
|
+
afirma('· happy: Codex completed user and agent items reach City live',
|
|
217
|
+
espera(lambda: any(
|
|
218
|
+
row.get('kind') == 'conversation.agent'
|
|
219
|
+
and row.get('summary') == 'fake codex response'
|
|
220
|
+
and row.get('thread') == state.get('id')
|
|
221
|
+
for row in lineas_json(activity_path)))
|
|
222
|
+
and all('[Agents City authenticated local bus]' not in row.get('summary', '')
|
|
223
|
+
and '<channel source="plugin:city:city-bus"'
|
|
224
|
+
not in row.get('summary', '')
|
|
225
|
+
for row in lineas_json(activity_path)),
|
|
226
|
+
lee(activity_path))
|
|
227
|
+
afirma('· non-happy: Codex private reasoning never enters activity storage',
|
|
228
|
+
'PRIVATE_FAKE_CODEX_REASONING' not in lee(activity_path),
|
|
229
|
+
lee(activity_path))
|
|
230
|
+
status = json.loads(lee(status_path)) if os.path.exists(status_path) else {}
|
|
231
|
+
afirma('· delivery remains native and never injects the terminal',
|
|
232
|
+
status.get('transport') == 'codex-app-server-ws'
|
|
233
|
+
and status.get('terminalInjection') is False,
|
|
234
|
+
str(status))
|
|
235
|
+
|
|
236
|
+
# A second assignment proves thread/resume subscribed the gateway to
|
|
237
|
+
# completion events. Without that subscription the first turn would
|
|
238
|
+
# remain permanently "active" and this request would never be sent.
|
|
239
|
+
opened_again = subprocess.run(
|
|
240
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
241
|
+
input=json.dumps({
|
|
242
|
+
'question': 'Prove the resumed Codex thread accepts a second turn.',
|
|
243
|
+
'desiredOutcome': 'A second ordered native acceptance.',
|
|
244
|
+
'definitionOfDone': ['the second provider turn starts'],
|
|
245
|
+
'participants': ['codex-agent'],
|
|
246
|
+
}), capture_output=True, text=True,
|
|
247
|
+
env=dict(env, CITY_BUS_ACTOR='seat'), timeout=12)
|
|
248
|
+
second_state = json.loads(opened_again.stdout) if opened_again.returncode == 0 else {}
|
|
249
|
+
afirma('· completion events unlock a second ordered bus assignment',
|
|
250
|
+
bool(second_state.get('id'))
|
|
251
|
+
and espera(lambda: len(solicitudes(capture)) == 2
|
|
252
|
+
and len(aprobaciones(capture)) == 2),
|
|
253
|
+
opened_again.stderr or lee(log_path))
|
|
254
|
+
comprueba('· both assignments stay on the one TUI-created thread',
|
|
255
|
+
[row['request']['params']['threadId'] for row in solicitudes(capture)],
|
|
256
|
+
['thread_tui', 'thread_tui'])
|
|
257
|
+
|
|
258
|
+
child_pid = int(lee(tui_pid).strip() or 0)
|
|
259
|
+
afirma('· closing the Codex TUI closes its gateway',
|
|
260
|
+
detiene_proceso(child_pid)
|
|
261
|
+
and espera(lambda: gateway.poll() is not None and not os.path.exists(status_path)),
|
|
262
|
+
lee(log_path))
|
|
263
|
+
|
|
264
|
+
# Non-happy path: without auto approval the connector must answer the
|
|
265
|
+
# provider request explicitly with no granted permissions. It still
|
|
266
|
+
# uses the native protocol and stays alive; it does not turn `never`
|
|
267
|
+
# into a mysterious tool failure.
|
|
268
|
+
stream.close()
|
|
269
|
+
stream = None
|
|
270
|
+
non_auto_log = os.path.join(base, 'gateway-non-auto.log')
|
|
271
|
+
stream = open(non_auto_log, 'w+', encoding='utf-8')
|
|
272
|
+
non_auto_env = dict(env)
|
|
273
|
+
non_auto_env.pop('CITY_OUTER_CAGE', None)
|
|
274
|
+
gateway = subprocess.Popen(
|
|
275
|
+
['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
|
|
276
|
+
'--cwd', repo, '--command', fake_codex, '--auto', '0', '--interactive', '0'],
|
|
277
|
+
env=non_auto_env, stdin=subprocess.DEVNULL,
|
|
278
|
+
stdout=stream, stderr=stream, text=True)
|
|
279
|
+
afirma('· non-happy: a non-auto native gateway still starts cleanly',
|
|
280
|
+
espera(lambda: os.path.exists(status_path)), lee(non_auto_log))
|
|
281
|
+
non_auto_opened = subprocess.run(
|
|
282
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
283
|
+
input=json.dumps({
|
|
284
|
+
'question': 'Prove non-auto permissions remain denied.',
|
|
285
|
+
'desiredOutcome': 'One explicit denial without a crash.',
|
|
286
|
+
'definitionOfDone': ['the provider receives an empty grant'],
|
|
287
|
+
'participants': ['codex-agent'],
|
|
288
|
+
}), capture_output=True, text=True,
|
|
289
|
+
env=dict(non_auto_env, CITY_BUS_ACTOR='seat'), timeout=12)
|
|
290
|
+
afirma('· non-happy: provider approval is answered instead of hanging',
|
|
291
|
+
non_auto_opened.returncode == 0
|
|
292
|
+
and espera(lambda: len(solicitudes(capture)) == 3
|
|
293
|
+
and len(aprobaciones(capture)) == 3),
|
|
294
|
+
non_auto_opened.stderr or lee(non_auto_log))
|
|
295
|
+
non_auto_params = solicitudes(capture)[-1]['request']['params']
|
|
296
|
+
non_auto_approval = aprobaciones(capture)[-1]['approval'].get('result', {})
|
|
297
|
+
afirma('· non-happy: no outer cage keeps Codex workspace confinement',
|
|
298
|
+
non_auto_params.get('sandboxPolicy', {}).get('type') == 'workspaceWrite'
|
|
299
|
+
and non_auto_params.get('sandboxPolicy', {}).get('networkAccess') is False,
|
|
300
|
+
json.dumps(non_auto_params))
|
|
301
|
+
afirma('· non-happy: disabled auto approval grants no requested capability',
|
|
302
|
+
non_auto_approval == {'permissions': {}, 'scope': 'turn'},
|
|
303
|
+
json.dumps(non_auto_approval))
|
|
304
|
+
gateway.terminate()
|
|
305
|
+
afirma('· non-happy: the non-auto gateway cleans up normally',
|
|
306
|
+
espera(lambda: gateway.poll() is not None and not os.path.exists(status_path)),
|
|
307
|
+
lee(non_auto_log))
|
|
308
|
+
|
|
309
|
+
# Non-happy path: a TUI process can stay alive without ever creating a
|
|
310
|
+
# usable thread. The gateway must time out and must not adopt a stale
|
|
311
|
+
# or unrelated thread from the app-server.
|
|
312
|
+
stream.close()
|
|
313
|
+
stream = None
|
|
314
|
+
open(behavior, 'w', encoding='utf-8').write('missing-tui-thread\n')
|
|
315
|
+
missing_log = os.path.join(base, 'gateway-missing-thread.log')
|
|
316
|
+
stream = open(missing_log, 'w+', encoding='utf-8')
|
|
317
|
+
missing_env = dict(env, CITY_CODEX_TUI_READY_TIMEOUT_MS='350')
|
|
318
|
+
gateway = subprocess.Popen(
|
|
319
|
+
['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
|
|
320
|
+
'--cwd', repo, '--command', fake_codex,
|
|
321
|
+
'--auto', '1', '--interactive', '1'],
|
|
322
|
+
env=missing_env, stdin=subprocess.DEVNULL,
|
|
323
|
+
stdout=stream, stderr=stream, text=True)
|
|
324
|
+
afirma('· non-happy: a TUI without a new thread fails visibly',
|
|
325
|
+
espera(lambda: gateway.poll() is not None)
|
|
326
|
+
and gateway.returncode != 0
|
|
327
|
+
and 'did not create a new thread' in lee(missing_log),
|
|
328
|
+
lee(missing_log))
|
|
329
|
+
afirma('· non-happy: a missing thread leaves no online gateway status',
|
|
330
|
+
not os.path.exists(status_path), lee(missing_log))
|
|
331
|
+
afirma('· non-happy: a missing Codex thread leaves durable diagnostics',
|
|
332
|
+
'gateway.start.failed' in lee(diagnostic_path)
|
|
333
|
+
and 'did not create a new thread' in lee(diagnostic_path),
|
|
334
|
+
lee(diagnostic_path))
|
|
335
|
+
|
|
336
|
+
# Non-happy path: a supported runtime can still fail in its own UI.
|
|
337
|
+
# That must be visible and terminal, never a silent downgrade to the
|
|
338
|
+
# gateway readline prompt or to terminal injection.
|
|
339
|
+
stream.close()
|
|
340
|
+
stream = None
|
|
341
|
+
failing_dir = os.path.join(base, 'failing')
|
|
342
|
+
os.makedirs(failing_dir)
|
|
343
|
+
failing_codex = os.path.join(failing_dir, 'codex')
|
|
344
|
+
open(behavior, 'w', encoding='utf-8').write('healthy\n')
|
|
345
|
+
open(failing_codex, 'w', encoding='utf-8').write(
|
|
346
|
+
'#!/bin/sh\necho "fake Codex TUI startup failure" >&2\nexit 42\n')
|
|
347
|
+
os.chmod(failing_codex, 0o755)
|
|
348
|
+
failure_log = os.path.join(base, 'gateway-failure.log')
|
|
349
|
+
stream = open(failure_log, 'w+', encoding='utf-8')
|
|
350
|
+
gateway = subprocess.Popen(
|
|
351
|
+
['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
|
|
352
|
+
'--cwd', repo, '--command', failing_codex,
|
|
353
|
+
'--auto', '1', '--interactive', '1'],
|
|
354
|
+
env=env, stdin=subprocess.DEVNULL, stdout=stream, stderr=stream, text=True)
|
|
355
|
+
afirma('· non-happy: a broken Codex TUI makes the gateway fail visibly',
|
|
356
|
+
espera(lambda: gateway.poll() is not None)
|
|
357
|
+
and gateway.returncode == 42
|
|
358
|
+
and 'fake Codex TUI startup failure' in lee(failure_log),
|
|
359
|
+
lee(failure_log))
|
|
360
|
+
failure = lee(failure_log)
|
|
361
|
+
afirma('· non-happy: failure cleans status without console or paste fallback',
|
|
362
|
+
not os.path.exists(status_path)
|
|
363
|
+
and 'city> ' not in failure
|
|
364
|
+
and 'terminalInjection' not in failure,
|
|
365
|
+
failure)
|
|
366
|
+
|
|
367
|
+
# Non-happy path: a stale global MCP entry may point to a local binary
|
|
368
|
+
# that was removed with an old worktree. The city must disable exactly
|
|
369
|
+
# that integration for both app-server and its remote TUI, without
|
|
370
|
+
# rewriting the owner's Codex config or disabling healthy MCPs.
|
|
371
|
+
if server and server.poll() is None:
|
|
372
|
+
server.terminate()
|
|
373
|
+
server.wait(timeout=3)
|
|
374
|
+
server = None
|
|
375
|
+
stale_bin = os.path.join(base, 'stale-bin')
|
|
376
|
+
os.makedirs(stale_bin)
|
|
377
|
+
stale_codex = os.path.join(stale_bin, 'codex')
|
|
378
|
+
stale_server_args = os.path.join(base, 'stale-server-args.txt')
|
|
379
|
+
stale_tui_args = os.path.join(base, 'stale-tui-args.txt')
|
|
380
|
+
stale_tui_pid = os.path.join(base, 'stale-tui.pid')
|
|
381
|
+
stale_capture = os.path.join(base, 'stale-codex.jsonl')
|
|
382
|
+
stale_behavior = os.path.join(base, 'stale-behavior.txt')
|
|
383
|
+
missing_mcp = os.path.join(base, 'removed-worktree', 'missing-sidecar')
|
|
384
|
+
mcp_listing = json.dumps([
|
|
385
|
+
{
|
|
386
|
+
'name': 'broken-local', 'enabled': True,
|
|
387
|
+
'transport': {'type': 'stdio', 'command': missing_mcp, 'cwd': None},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
'name': 'healthy-local', 'enabled': True,
|
|
391
|
+
'transport': {'type': 'stdio', 'command': sys.executable, 'cwd': None},
|
|
392
|
+
},
|
|
393
|
+
])
|
|
394
|
+
open(stale_behavior, 'w', encoding='utf-8').write('healthy\n')
|
|
395
|
+
open(stale_codex, 'w', encoding='utf-8').write(
|
|
396
|
+
f'#!{sys.executable}\n'
|
|
397
|
+
'import os, signal, sys, time\n'
|
|
398
|
+
'args = sys.argv[1:]\n'
|
|
399
|
+
f'listing = {mcp_listing!r}\n'
|
|
400
|
+
'if args == ["mcp", "list", "--json"]:\n'
|
|
401
|
+
' print(listing)\n'
|
|
402
|
+
' raise SystemExit(0)\n'
|
|
403
|
+
'if args and args[0] == "app-server":\n'
|
|
404
|
+
' open(os.environ["CITY_TEST_SERVER_ARGS"], "w").write("\\n".join(args) + "\\n")\n'
|
|
405
|
+
' url = args[args.index("--listen") + 1]\n'
|
|
406
|
+
' port = url.rsplit(":", 1)[1]\n'
|
|
407
|
+
f' os.execv({shutil.which("node")!r}, '
|
|
408
|
+
f'[{shutil.which("node")!r}, {FAKE!r}, "codex", port, '
|
|
409
|
+
'os.environ["CITY_TEST_STALE_CAPTURE"], '
|
|
410
|
+
'os.environ["CITY_TEST_STALE_BEHAVIOR"]])\n'
|
|
411
|
+
'if "--remote" in args:\n'
|
|
412
|
+
' open(os.environ["CITY_TEST_STALE_TUI_ARGS"], "w").write('
|
|
413
|
+
'"\\n".join(args) + "\\n")\n'
|
|
414
|
+
' open(os.environ["CITY_TEST_STALE_TUI_PID"], "w").write('
|
|
415
|
+
'str(os.getpid()) + "\\n")\n'
|
|
416
|
+
' def stop(_signal, _frame): raise SystemExit(0)\n'
|
|
417
|
+
' signal.signal(signal.SIGINT, stop)\n'
|
|
418
|
+
' signal.signal(signal.SIGTERM, stop)\n'
|
|
419
|
+
' signal.signal(signal.SIGHUP, stop)\n'
|
|
420
|
+
' while True: time.sleep(1)\n'
|
|
421
|
+
'raise SystemExit(64)\n')
|
|
422
|
+
os.chmod(stale_codex, 0o755)
|
|
423
|
+
stale_env = dict(env,
|
|
424
|
+
CITY_TEST_SERVER_ARGS=stale_server_args,
|
|
425
|
+
CITY_TEST_STALE_TUI_ARGS=stale_tui_args,
|
|
426
|
+
CITY_TEST_STALE_TUI_PID=stale_tui_pid,
|
|
427
|
+
CITY_TEST_STALE_CAPTURE=stale_capture,
|
|
428
|
+
CITY_TEST_STALE_BEHAVIOR=stale_behavior)
|
|
429
|
+
stale_env.pop('CITY_CODEX_APP_SERVER_URL', None)
|
|
430
|
+
stale_env.pop('CITY_OUTER_CAGE', None)
|
|
431
|
+
stale_log = os.path.join(base, 'gateway-stale-mcp.log')
|
|
432
|
+
stream.close()
|
|
433
|
+
stream = open(stale_log, 'w+', encoding='utf-8')
|
|
434
|
+
gateway = subprocess.Popen(
|
|
435
|
+
['node', GATEWAY, '--data', city, '--actor', 'codex-agent',
|
|
436
|
+
'--cwd', repo, '--command', stale_codex,
|
|
437
|
+
'--auto', '1', '--interactive', '1'],
|
|
438
|
+
env=stale_env, stdin=subprocess.DEVNULL,
|
|
439
|
+
stdout=stream, stderr=stream, text=True)
|
|
440
|
+
afirma('· non-happy: Codex starts even when one global MCP executable is gone',
|
|
441
|
+
espera(lambda: os.path.exists(status_path)
|
|
442
|
+
and os.path.exists(stale_server_args)
|
|
443
|
+
and os.path.exists(stale_tui_args)
|
|
444
|
+
and os.path.exists(stale_tui_pid)),
|
|
445
|
+
lee(stale_log))
|
|
446
|
+
server_arguments = lee(stale_server_args).splitlines()
|
|
447
|
+
stale_ui_arguments = lee(stale_tui_args).splitlines()
|
|
448
|
+
expected_override = 'mcp_servers.broken-local.enabled=false'
|
|
449
|
+
afirma('· only the unavailable MCP is disabled for app-server and the TUI',
|
|
450
|
+
expected_override in server_arguments
|
|
451
|
+
and expected_override in stale_ui_arguments
|
|
452
|
+
and not any('healthy-local' in value
|
|
453
|
+
for value in server_arguments + stale_ui_arguments),
|
|
454
|
+
json.dumps({'server': server_arguments, 'tui': stale_ui_arguments}))
|
|
455
|
+
afirma('· the owner config is untouched and the scoped MCP decision is diagnostic',
|
|
456
|
+
any(row.get('event') == 'codex.mcp.unavailable.disabled'
|
|
457
|
+
and row.get('message') == 'broken-local'
|
|
458
|
+
for row in lineas_json(diagnostic_path)),
|
|
459
|
+
lee(diagnostic_path)[-1200:])
|
|
460
|
+
stale_child_pid = int(lee(stale_tui_pid).strip() or 0)
|
|
461
|
+
afirma('· stale-MCP test gateway closes with its official TUI',
|
|
462
|
+
detiene_proceso(stale_child_pid)
|
|
463
|
+
and espera(lambda: gateway.poll() is not None
|
|
464
|
+
and not os.path.exists(status_path)),
|
|
465
|
+
lee(stale_log))
|
|
466
|
+
finally:
|
|
467
|
+
if gateway and gateway.poll() is None:
|
|
468
|
+
gateway.terminate()
|
|
469
|
+
try:
|
|
470
|
+
gateway.wait(timeout=3)
|
|
471
|
+
except subprocess.TimeoutExpired:
|
|
472
|
+
gateway.kill()
|
|
473
|
+
if server and server.poll() is None:
|
|
474
|
+
server.terminate()
|
|
475
|
+
try:
|
|
476
|
+
server.wait(timeout=3)
|
|
477
|
+
except subprocess.TimeoutExpired:
|
|
478
|
+
server.kill()
|
|
479
|
+
if stream:
|
|
480
|
+
stream.close()
|
|
481
|
+
afirma('· interactive cleanup leaves no orphan city hub',
|
|
482
|
+
detiene_hubs_de_ciudad(city), str(city))
|
|
483
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
484
|
+
return resumen('runtime-ui')
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == '__main__':
|
|
488
|
+
sys.exit(main())
|