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,252 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The cage: one macOS seatbelt profile per repo window.
|
|
3
|
+
|
|
4
|
+
The yolo flag stays — a committee cannot work if every bus command needs a
|
|
5
|
+
human. What changes is what the kernel lets the window touch. An agent inside
|
|
6
|
+
the cage is never asked anything: writes land only in its own repo and the
|
|
7
|
+
runtime state it legitimately owns, and the files that turn a prompt injection
|
|
8
|
+
into a credential theft (`~/.ssh`, `~/.git-credentials`, cloud configs, the
|
|
9
|
+
`gh` token) simply do not exist for it.
|
|
10
|
+
|
|
11
|
+
Seatbelt semantics, verified on a real machine before this was written:
|
|
12
|
+
the LAST matching rule wins, children and grandchildren inherit the profile,
|
|
13
|
+
and the network stays open unless denied. So the profile reads top to bottom
|
|
14
|
+
as: allow everything, forbid all writes, re-allow the working set, and finally
|
|
15
|
+
seal the secrets — reads and writes both.
|
|
16
|
+
|
|
17
|
+
Honest limits, documented rather than hidden: credentials held in the macOS
|
|
18
|
+
keychain travel over IPC, not file reads, so a jailed `git push` still works
|
|
19
|
+
when the owner keeps GitHub credentials there; outbound network stays open;
|
|
20
|
+
and every window still runs as the same OS user. The cage narrows what a
|
|
21
|
+
compromised window can steal — it is not hostile-process isolation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import shutil
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
import rutas
|
|
31
|
+
|
|
32
|
+
# Read+write denied inside the cage: directories that exist to hold credentials.
|
|
33
|
+
SECRETOS_DIR = (
|
|
34
|
+
'.ssh', '.aws', '.kube', '.gnupg',
|
|
35
|
+
os.path.join('.config', 'gcloud'),
|
|
36
|
+
os.path.join('.config', 'gh'),
|
|
37
|
+
os.path.join('.docker'),
|
|
38
|
+
)
|
|
39
|
+
# Single files with the same job. `~/.npmrc` is deliberately NOT here: denying
|
|
40
|
+
# it breaks every `npm install` for owners who configure a registry, and a
|
|
41
|
+
# broken product protects nobody. Owners who keep tokens there add it through
|
|
42
|
+
# CITY_CAGE_DENY.
|
|
43
|
+
SECRETOS_FICHERO = ('.git-credentials', '.netrc', '.pgpass')
|
|
44
|
+
|
|
45
|
+
# Writable beyond the repo: the runtime state and build caches a working agent
|
|
46
|
+
# legitimately owns. Everything here is cache or agent state, never a secret —
|
|
47
|
+
# with the exceptions sealed again further down.
|
|
48
|
+
ESCRITURA_CASA = (
|
|
49
|
+
'.claude', '.agents-city', '.codex', '.opencode', '.kimi',
|
|
50
|
+
'.npm', '.cache', '.cargo', '.rustup', '.gradle', '.m2',
|
|
51
|
+
'.pnpm-store', '.bun',
|
|
52
|
+
os.path.join('Library', 'Caches'),
|
|
53
|
+
os.path.join('Library', 'Logs'),
|
|
54
|
+
os.path.join('Library', 'pnpm'),
|
|
55
|
+
os.path.join('Library', 'Developer'),
|
|
56
|
+
)
|
|
57
|
+
ESCRITURA_SISTEMA = ('/dev', '/private/tmp', '/private/var/tmp', '/private/var/folders')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sbpl(ruta):
|
|
61
|
+
"""Quote one path for SBPL: backslashes and double quotes escaped."""
|
|
62
|
+
return '"' + ruta.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def agents_home(casa):
|
|
66
|
+
"""The Agents City state root, honouring AGENTS_CITY_HOME like the broker.
|
|
67
|
+
|
|
68
|
+
The broker writes its token store under this root (`broker.estado_de`), so
|
|
69
|
+
the cage must seal the *same* root it uses — not a hardcoded `~/.agents-city`
|
|
70
|
+
that silently misses the broker dir when the owner relocated the home.
|
|
71
|
+
"""
|
|
72
|
+
hogar = os.environ.get('AGENTS_CITY_HOME') or os.path.join(casa, '.agents-city')
|
|
73
|
+
return rutas.canonicaliza(hogar)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sellados(casa):
|
|
77
|
+
"""The canonical set of read+write-sealed roots.
|
|
78
|
+
|
|
79
|
+
The single source of truth for "must never be reachable": the deny lines
|
|
80
|
+
emit it, and the write-allow filter refuses any owner path that re-enters
|
|
81
|
+
it. Extra owner denies (CITY_CAGE_DENY) join it, so an owner can widen the
|
|
82
|
+
seal but the allow-list can never outrun it.
|
|
83
|
+
"""
|
|
84
|
+
ac = agents_home(casa)
|
|
85
|
+
extra = [p for p in os.environ.get('CITY_CAGE_DENY', '').split(':') if p]
|
|
86
|
+
raices = [os.path.join(casa, d) for d in SECRETOS_DIR]
|
|
87
|
+
raices += [os.path.join(casa, f) for f in SECRETOS_FICHERO]
|
|
88
|
+
raices += [os.path.expanduser(p) for p in extra]
|
|
89
|
+
raices.append(os.path.join(ac, '.runtime', 'broker'))
|
|
90
|
+
raices.append(os.path.join(casa, '.cargo', 'credentials'))
|
|
91
|
+
raices.append(os.path.join(casa, '.cargo', 'credentials.toml'))
|
|
92
|
+
return [rutas.canonicaliza(r) for r in raices]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _permitidas_escritura(repo, casa, bloqueados, extra_escritura=()):
|
|
96
|
+
"""Canonical write-allow roots, given the already-computed sealed set.
|
|
97
|
+
|
|
98
|
+
Covering roots are intentionally kept: the sealed-secret block is emitted
|
|
99
|
+
last and, under seatbelt's last-match-wins rule, re-denies any secret that
|
|
100
|
+
a broad writable ancestor would otherwise expose. So the ordering — not a
|
|
101
|
+
filter here — is the guarantee, and `bin/test-cage.py` proves it against the
|
|
102
|
+
live kernel. What this pass does add is a loud drop of an owner extra that
|
|
103
|
+
resolves strictly *inside* a sealed root: that write is already dead under
|
|
104
|
+
the final deny, so honouring it silently would only mislead. The relocated
|
|
105
|
+
state home is added explicitly so a custom AGENTS_CITY_HOME stays writable.
|
|
106
|
+
|
|
107
|
+
`extra_escritura` is where the agent-first model plugs in: the resolved
|
|
108
|
+
targets of the agent's workspace mounts (symlinks already followed to their
|
|
109
|
+
real path). They pass through the same sealed-root check as any owner extra.
|
|
110
|
+
"""
|
|
111
|
+
extra = [os.path.expanduser(p)
|
|
112
|
+
for p in os.environ.get('CITY_CAGE_ALLOW_WRITE', '').split(':') if p]
|
|
113
|
+
extra += list(extra_escritura)
|
|
114
|
+
fijas = [repo, agents_home(casa), *ESCRITURA_SISTEMA]
|
|
115
|
+
fijas += [os.path.join(casa, p) for p in ESCRITURA_CASA]
|
|
116
|
+
aceptadas = [rutas.canonicaliza(c) for c in fijas]
|
|
117
|
+
for c in extra:
|
|
118
|
+
if any(rutas.dentro_de(c, raiz) for raiz in bloqueados):
|
|
119
|
+
sys.stderr.write(f'cage: ignoring write-allow {c}: inside a sealed root\n')
|
|
120
|
+
continue
|
|
121
|
+
aceptadas.append(rutas.canonicaliza(c))
|
|
122
|
+
return aceptadas
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _lineas_permite_escritura(repo, casa, bloqueados, extra_escritura=()):
|
|
126
|
+
permitidas = _permitidas_escritura(repo, casa, bloqueados, extra_escritura)
|
|
127
|
+
lineas = [f' (subpath {_sbpl(r)})' for r in permitidas]
|
|
128
|
+
# Claude keeps `~/.claude.json` (and its backups) at the HOME root.
|
|
129
|
+
lineas.append(f' (regex #"^{re.escape(os.path.join(casa, ".claude.json"))}")')
|
|
130
|
+
return lineas
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _lineas_secretos(casa, bloqueados):
|
|
134
|
+
lineas = []
|
|
135
|
+
for raiz in bloqueados:
|
|
136
|
+
# A file target uses `literal`; a directory (or a not-yet-created dir)
|
|
137
|
+
# uses `subpath` so everything beneath it is sealed too.
|
|
138
|
+
if os.path.isfile(raiz):
|
|
139
|
+
lineas.append(f' (literal {_sbpl(raiz)})')
|
|
140
|
+
else:
|
|
141
|
+
lineas.append(f' (subpath {_sbpl(raiz)})')
|
|
142
|
+
# Remote road tokens configured for the seat: repo windows must never read
|
|
143
|
+
# them, even though the rest of `~/.claude` stays open for the runtime.
|
|
144
|
+
canal = re.escape(os.path.join(casa, '.claude', 'channels'))
|
|
145
|
+
lineas.append(f' (regex #"^{canal}/[^/]+/\\.env$")')
|
|
146
|
+
return lineas
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def perfil(repo, casa=None, fichero_token=None, extra_escritura=()):
|
|
150
|
+
"""The SBPL profile for one window, as text. Pure: no filesystem writes.
|
|
151
|
+
|
|
152
|
+
`repo` is the window's working directory (a repo, or an agent workspace).
|
|
153
|
+
`extra_escritura` are additional writable roots — the resolved targets of an
|
|
154
|
+
agent's workspace mounts — sealed-root-checked like any other write-allow.
|
|
155
|
+
"""
|
|
156
|
+
casa = rutas.canonicaliza(casa or '~')
|
|
157
|
+
repo = rutas.canonicaliza(repo)
|
|
158
|
+
if not os.path.isdir(repo):
|
|
159
|
+
raise ValueError(f'the cage needs an existing working directory, got: {repo}')
|
|
160
|
+
# Compute the sealed set once and thread it through every helper below.
|
|
161
|
+
bloqueados = sellados(casa)
|
|
162
|
+
motivo = rutas.motivo_bloqueo(repo, bloqueados)
|
|
163
|
+
if motivo:
|
|
164
|
+
raise ValueError(f'the working directory is unsafe to cage: {motivo}')
|
|
165
|
+
partes = [
|
|
166
|
+
'(version 1)',
|
|
167
|
+
'; Agents City cage — generated, one per window. Do not edit.',
|
|
168
|
+
'(allow default)',
|
|
169
|
+
'(deny file-write*)',
|
|
170
|
+
'(allow file-write*',
|
|
171
|
+
*_lineas_permite_escritura(repo, casa, bloqueados, extra_escritura),
|
|
172
|
+
')',
|
|
173
|
+
'(deny file-read* file-write*',
|
|
174
|
+
*_lineas_secretos(casa, bloqueados),
|
|
175
|
+
')',
|
|
176
|
+
]
|
|
177
|
+
if fichero_token:
|
|
178
|
+
# This window's own broker token, and only this window's: the broker
|
|
179
|
+
# directory deny above stays in force for every other file in it.
|
|
180
|
+
partes.append(f'(allow file-read* (literal {_sbpl(rutas.canonicaliza(fichero_token))}))')
|
|
181
|
+
return '\n'.join(partes) + '\n'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def disponible():
|
|
185
|
+
"""Whether this machine can cage a window at all."""
|
|
186
|
+
if os.environ.get('CITY_CAGE', '1') == '0':
|
|
187
|
+
return False
|
|
188
|
+
return sys.platform == 'darwin' and shutil.which('sandbox-exec') is not None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def escribe_perfil(repo, ventana, casa=None, fichero_token=None, extra_escritura=()):
|
|
192
|
+
"""Write the profile under the runtime dir and return its path."""
|
|
193
|
+
casa_real = os.path.realpath(casa or os.path.expanduser('~'))
|
|
194
|
+
directorio = os.path.join(casa_real, '.agents-city', '.runtime', 'cage')
|
|
195
|
+
os.makedirs(directorio, mode=0o700, exist_ok=True)
|
|
196
|
+
segmento = re.sub(r'[^a-zA-Z0-9_-]', '-', ventana) or 'window'
|
|
197
|
+
ruta = os.path.join(directorio, f'{segmento}.sb')
|
|
198
|
+
contenido = perfil(repo, casa=casa_real, fichero_token=fichero_token,
|
|
199
|
+
extra_escritura=extra_escritura)
|
|
200
|
+
with open(ruta, 'w', encoding='utf-8') as f:
|
|
201
|
+
f.write(contenido)
|
|
202
|
+
os.chmod(ruta, 0o600)
|
|
203
|
+
return ruta
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def linea(repo, ventana, casa=None, fichero_token=None, extra_escritura=()):
|
|
207
|
+
"""The launch prefix for one window: `sandbox-exec -f <profile> `, or ''.
|
|
208
|
+
|
|
209
|
+
Empty means "launch uncaged": not macOS, no sandbox-exec, or the owner set
|
|
210
|
+
CITY_CAGE=0. The caller prepends the result verbatim, so the degraded path
|
|
211
|
+
is exactly the behaviour the product always had.
|
|
212
|
+
"""
|
|
213
|
+
if not disponible():
|
|
214
|
+
return ''
|
|
215
|
+
ruta = escribe_perfil(repo, ventana, casa=casa, fichero_token=fichero_token,
|
|
216
|
+
extra_escritura=extra_escritura)
|
|
217
|
+
return f'sandbox-exec -f {ruta} '
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main():
|
|
221
|
+
p = argparse.ArgumentParser(description='One seatbelt cage per window.')
|
|
222
|
+
p.add_argument('orden', choices=['line', 'profile', 'check'])
|
|
223
|
+
p.add_argument('--repo', help='the working directory this window owns')
|
|
224
|
+
p.add_argument('--window', default='window', help='window name, used to name the profile')
|
|
225
|
+
p.add_argument('--home', help='HOME override (tests only)')
|
|
226
|
+
p.add_argument('--token-file', help="this window's broker token file, allowed read-only")
|
|
227
|
+
p.add_argument('--mounts', default='',
|
|
228
|
+
help='colon-separated resolved mount targets to also make writable')
|
|
229
|
+
args = p.parse_args()
|
|
230
|
+
if args.orden == 'check':
|
|
231
|
+
print('cage available' if disponible() else 'cage unavailable on this machine')
|
|
232
|
+
return 0 if disponible() else 1
|
|
233
|
+
if not args.repo:
|
|
234
|
+
print('--repo is required', file=sys.stderr)
|
|
235
|
+
return 2
|
|
236
|
+
extra = tuple(m for m in args.mounts.split(':') if m)
|
|
237
|
+
try:
|
|
238
|
+
if args.orden == 'profile':
|
|
239
|
+
sys.stdout.write(perfil(args.repo, casa=args.home, fichero_token=args.token_file,
|
|
240
|
+
extra_escritura=extra))
|
|
241
|
+
else:
|
|
242
|
+
sys.stdout.write(
|
|
243
|
+
linea(args.repo, args.window, casa=args.home, fichero_token=args.token_file,
|
|
244
|
+
extra_escritura=extra))
|
|
245
|
+
except (OSError, ValueError) as e:
|
|
246
|
+
print(f'{e}', file=sys.stderr)
|
|
247
|
+
return 1
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == '__main__':
|
|
252
|
+
sys.exit(main())
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live, read-only recognition of skills available to a city's repo agents.
|
|
3
|
+
|
|
4
|
+
Agents City never installs, copies, enables or edits a skill. The runtime in a
|
|
5
|
+
repo remains the source of truth and decides whether a skill applies to a task.
|
|
6
|
+
This module only discovers conventional repo-local ``SKILL.md`` files so a seat
|
|
7
|
+
can see which agent is likely to know how to help.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import card
|
|
16
|
+
import cities
|
|
17
|
+
import workspace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RAICES = (".claude/skills", ".codex/skills", ".agents/skills", "skills")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _campo(texto, clave):
|
|
24
|
+
if not texto.startswith("---"):
|
|
25
|
+
return ""
|
|
26
|
+
partes = texto.split("---", 2)
|
|
27
|
+
if len(partes) < 3:
|
|
28
|
+
return ""
|
|
29
|
+
m = re.search(rf"^{re.escape(clave)}:[ \t]*(.+)$", partes[1], re.M)
|
|
30
|
+
return m.group(1).strip().strip("\"'") if m else ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def descubre_repo(ruta_repo):
|
|
34
|
+
"""Skills declared by this repo, with no cache and no writes."""
|
|
35
|
+
fuera, vistos = [], set()
|
|
36
|
+
raiz_manifest = os.path.join(ruta_repo, "SKILL.md")
|
|
37
|
+
if os.path.isfile(raiz_manifest):
|
|
38
|
+
try:
|
|
39
|
+
texto = open(raiz_manifest, encoding="utf-8").read()
|
|
40
|
+
except OSError:
|
|
41
|
+
texto = ""
|
|
42
|
+
if texto:
|
|
43
|
+
real = os.path.realpath(raiz_manifest)
|
|
44
|
+
vistos.add(real)
|
|
45
|
+
fuera.append(
|
|
46
|
+
{
|
|
47
|
+
"name": _campo(texto, "name") or os.path.basename(ruta_repo),
|
|
48
|
+
"description": _campo(texto, "description"),
|
|
49
|
+
"manifest": real,
|
|
50
|
+
"provider": "repo",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
for relativa in RAICES:
|
|
54
|
+
base = os.path.join(ruta_repo, relativa)
|
|
55
|
+
if not os.path.isdir(base):
|
|
56
|
+
continue
|
|
57
|
+
for nombre in sorted(os.listdir(base)):
|
|
58
|
+
manifiesto = os.path.join(base, nombre, "SKILL.md")
|
|
59
|
+
if not os.path.isfile(manifiesto):
|
|
60
|
+
continue
|
|
61
|
+
real = os.path.realpath(manifiesto)
|
|
62
|
+
if real in vistos:
|
|
63
|
+
continue
|
|
64
|
+
vistos.add(real)
|
|
65
|
+
try:
|
|
66
|
+
texto = open(manifiesto, encoding="utf-8").read()
|
|
67
|
+
except OSError:
|
|
68
|
+
continue
|
|
69
|
+
fuera.append(
|
|
70
|
+
{
|
|
71
|
+
"name": _campo(texto, "name") or nombre,
|
|
72
|
+
"description": _campo(texto, "description"),
|
|
73
|
+
"manifest": real,
|
|
74
|
+
"provider": relativa.split("/", 1)[0].lstrip(".") or "repo",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return fuera
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _indice_repos():
|
|
81
|
+
guion = os.path.join(os.path.dirname(__file__), "find-repos.sh")
|
|
82
|
+
try:
|
|
83
|
+
salida = subprocess.run([guion], capture_output=True, text=True, timeout=300).stdout
|
|
84
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
85
|
+
return {}
|
|
86
|
+
fuera = {}
|
|
87
|
+
for linea in salida.splitlines():
|
|
88
|
+
if "\t" in linea:
|
|
89
|
+
nombre, ruta = linea.split("\t", 1)
|
|
90
|
+
fuera[nombre.strip()] = ruta.strip()
|
|
91
|
+
return fuera
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ficha_de_ciudad(datos):
|
|
95
|
+
owner = cities.lee_clave(datos, "owner")
|
|
96
|
+
preferida = os.path.join(datos, f"{owner}.md") if owner else ""
|
|
97
|
+
if preferida and os.path.isfile(preferida):
|
|
98
|
+
return preferida
|
|
99
|
+
try:
|
|
100
|
+
fichas = sorted(
|
|
101
|
+
os.path.join(datos, f)
|
|
102
|
+
for f in os.listdir(datos)
|
|
103
|
+
if f.endswith(".md") and card.lee(os.path.join(datos, f)).get("user")
|
|
104
|
+
)
|
|
105
|
+
except OSError:
|
|
106
|
+
fichas = []
|
|
107
|
+
return fichas[0] if fichas else ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ruta_de(nombre, rutas=None):
|
|
111
|
+
"""One legacy repo's folder on this disk, or ''. The single mechanism both
|
|
112
|
+
full discovery and one-agent lookups share — resolving one path must never
|
|
113
|
+
cost a whole-city skill scan."""
|
|
114
|
+
rutas = _indice_repos() if rutas is None else rutas
|
|
115
|
+
code = os.path.expanduser(os.environ.get("CITY_CODE_DIR", "~/codigo"))
|
|
116
|
+
ruta = rutas.get(nombre, os.path.join(code, nombre))
|
|
117
|
+
return os.path.realpath(ruta) if os.path.isdir(ruta) else ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def descubre_ciudad(datos, rutas=None):
|
|
121
|
+
"""``{agent: {path, role, skills}}`` for the city's support agents.
|
|
122
|
+
|
|
123
|
+
Both card shapes, through the one normaliser (`workspace.agentes`): a
|
|
124
|
+
legacy repo agent looks for its repo on disk exactly as before, and an
|
|
125
|
+
agents-first agent looks in its own workspace and in every mount it
|
|
126
|
+
declares. Before this, an agents-first city honestly full of skills showed
|
|
127
|
+
"Agents & skills 0" — the discovery only knew `repos:`.
|
|
128
|
+
"""
|
|
129
|
+
ficha = ficha_de_ciudad(datos)
|
|
130
|
+
if not ficha:
|
|
131
|
+
return {}
|
|
132
|
+
try:
|
|
133
|
+
texto = open(ficha, encoding="utf-8").read()
|
|
134
|
+
except OSError:
|
|
135
|
+
return {}
|
|
136
|
+
try:
|
|
137
|
+
agentes = workspace.agentes(texto, datos)
|
|
138
|
+
except ValueError:
|
|
139
|
+
# A malformed card (two names that slug to one identity) is the card
|
|
140
|
+
# tools' error to raise loudly; read-only discovery just has nothing
|
|
141
|
+
# safe to list for it.
|
|
142
|
+
return {}
|
|
143
|
+
if not agentes:
|
|
144
|
+
return {}
|
|
145
|
+
# The disk index is a find-repos scan: paid lazily, and only when a legacy
|
|
146
|
+
# agent actually needs it — a pure agents-first city never touches it.
|
|
147
|
+
fuera = {}
|
|
148
|
+
for a in agentes:
|
|
149
|
+
if a.legacy:
|
|
150
|
+
rutas = _indice_repos() if rutas is None else rutas
|
|
151
|
+
candidatos = [ruta_de(a.nombre, rutas)]
|
|
152
|
+
else:
|
|
153
|
+
candidatos = [a.workspace] + [os.path.expanduser(m) for m in a.mounts]
|
|
154
|
+
existentes = [c for c in candidatos if c and os.path.isdir(c)]
|
|
155
|
+
skills = []
|
|
156
|
+
vistos = set()
|
|
157
|
+
for c in existentes:
|
|
158
|
+
for s in descubre_repo(c):
|
|
159
|
+
if s["name"] in vistos:
|
|
160
|
+
continue
|
|
161
|
+
vistos.add(s["name"])
|
|
162
|
+
skills.append(s)
|
|
163
|
+
fuera[a.nombre] = {
|
|
164
|
+
"path": os.path.realpath(existentes[0]) if existentes else "",
|
|
165
|
+
"role": a.rol,
|
|
166
|
+
"skills": skills,
|
|
167
|
+
}
|
|
168
|
+
return fuera
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main():
|
|
172
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help", "help"):
|
|
173
|
+
print(
|
|
174
|
+
"usage: agents-city skills [city]\n\n"
|
|
175
|
+
"Read-only discovery of skills already installed in each repo."
|
|
176
|
+
)
|
|
177
|
+
return 0
|
|
178
|
+
usuario = cities.usuario_actual()
|
|
179
|
+
pedida = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
180
|
+
datos = cities.resuelve(pedida, usuario) if pedida else cities.actual(usuario)
|
|
181
|
+
if not datos:
|
|
182
|
+
print(f" No city called {pedida!r}.", file=sys.stderr)
|
|
183
|
+
return 1
|
|
184
|
+
capacidades = descubre_ciudad(datos)
|
|
185
|
+
print(f" Skills visible to {cities.direccion(usuario, datos)}")
|
|
186
|
+
if not capacidades:
|
|
187
|
+
print(" No repo agents in this city.")
|
|
188
|
+
for repo, info in capacidades.items():
|
|
189
|
+
nombres = ", ".join(s["name"] for s in info["skills"]) or "none discovered"
|
|
190
|
+
estado = info["path"] or "repo not found on this machine"
|
|
191
|
+
print(f" {repo} [{info['role']}]: {nombres}\n {estado}")
|
|
192
|
+
print("\n Read-only discovery: Agents City installed and changed nothing.")
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
sys.exit(main())
|