agents-city 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2226 -0
- package/README.md +2196 -0
- package/benchmarks/committee/README.md +39 -0
- package/benchmarks/committee/metrics.py +51 -0
- package/benchmarks/committee/run.py +64 -0
- package/benchmarks/committee/traces/01-single.json +8 -0
- package/benchmarks/committee/traces/02-mesh.json +16 -0
- package/benchmarks/committee/traces/03-chair.json +15 -0
- package/benchmarks/latency/README.md +35 -0
- package/benchmarks/latency/fake-claude-cli.mjs +119 -0
- package/benchmarks/latency/fake-native-server.mjs +340 -0
- package/benchmarks/latency/fake-tui.py +91 -0
- package/benchmarks/latency/live.py +380 -0
- package/benchmarks/stress/README.md +20 -0
- package/benchmarks/stress/run.py +487 -0
- package/bin/agents +4 -0
- package/bin/agents-city.js +119 -0
- package/bin/benchmark +17 -0
- package/bin/bus +6 -0
- package/bin/cities +3 -0
- package/bin/city +86 -0
- package/bin/committee +6 -0
- package/bin/demo +200 -0
- package/bin/doctor +3 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +498 -0
- package/bin/logs +3 -0
- package/bin/navegador.mjs +243 -0
- package/bin/report.py +14 -0
- package/bin/reset +3 -0
- package/bin/road +3 -0
- package/bin/seat +10 -0
- package/bin/serve.py +1579 -0
- package/bin/setup.py +753 -0
- package/bin/shortcut +3 -0
- package/bin/skills +3 -0
- package/bin/test +133 -0
- package/bin/test-actualiza.py +130 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-avatar.py +58 -0
- package/bin/test-benchmark.py +40 -0
- package/bin/test-broker.py +334 -0
- package/bin/test-cage.py +374 -0
- package/bin/test-card.py +402 -0
- package/bin/test-channel.py +379 -0
- package/bin/test-cities.py +453 -0
- package/bin/test-claude-runtime.py +252 -0
- package/bin/test-committee.py +340 -0
- package/bin/test-contracts.py +551 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +139 -0
- package/bin/test-domains.py +151 -0
- package/bin/test-evidencia.py +50 -0
- package/bin/test-exit.py +128 -0
- package/bin/test-hall-protocol.py +93 -0
- package/bin/test-launch.py +169 -0
- package/bin/test-live-feed.py +508 -0
- package/bin/test-navegador.py +140 -0
- package/bin/test-pairing.py +99 -0
- package/bin/test-parcels.py +283 -0
- package/bin/test-runtime-failures.py +252 -0
- package/bin/test-runtime-ui.py +488 -0
- package/bin/test-runtime.py +268 -0
- package/bin/test-rutas.py +80 -0
- package/bin/test-seat.py +1598 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1251 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +251 -0
- package/bin/testlib.py +118 -0
- package/bin/tokens.py +14 -0
- package/bin/update +3 -0
- package/bus/scripts/deploy.sh +60 -0
- package/bus/scripts/mint-token.sh +68 -0
- package/bus/scripts/setup-dev.sh +79 -0
- package/bus/scripts/test-channel.ts +8 -0
- package/bus/scripts/test-hub.ts +266 -0
- package/bus/scripts/test-local.sh +40 -0
- package/bus/scripts/test-queue.ts +132 -0
- package/bus/worker/package-lock.json +1971 -0
- package/bus/worker/package.json +17 -0
- package/bus/worker/src/index.ts +485 -0
- package/bus/worker/tsconfig.json +13 -0
- package/bus/worker/wrangler.toml +32 -0
- package/city/oven/README.md +59 -0
- package/city/oven/collect.py +43 -0
- package/city/oven/oven.html +4 -0
- package/city/scripts/history.py +120 -0
- package/city/scripts/seed.py +442 -0
- package/city/web/assets/modelos/License.txt +28 -0
- package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/assets/modelos/building-a.glb +0 -0
- package/city/web/assets/modelos/building-b.glb +0 -0
- package/city/web/assets/modelos/building-c.glb +0 -0
- package/city/web/assets/modelos/building-d.glb +0 -0
- package/city/web/assets/modelos/building-e.glb +0 -0
- package/city/web/assets/modelos/building-f.glb +0 -0
- package/city/web/assets/modelos/building-g.glb +0 -0
- package/city/web/assets/modelos/building-h.glb +0 -0
- package/city/web/assets/sprites/building-a.png +0 -0
- package/city/web/assets/sprites/building-b.png +0 -0
- package/city/web/assets/sprites/building-c.png +0 -0
- package/city/web/assets/sprites/building-d.png +0 -0
- package/city/web/assets/sprites/building-e.png +0 -0
- package/city/web/assets/sprites/building-f.png +0 -0
- package/city/web/assets/sprites/building-g.png +0 -0
- package/city/web/assets/sprites/building-h.png +0 -0
- package/city/web/assets/sprites/building-i.png +0 -0
- package/city/web/assets/sprites/building-j.png +0 -0
- package/city/web/assets/sprites/building-k.png +0 -0
- package/city/web/assets/sprites/building-l.png +0 -0
- package/city/web/assets/sprites/building-m.png +0 -0
- package/city/web/assets/sprites/building-n.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
- package/city/web/assets/sprites/catalogo.json +47 -0
- package/city/web/assets/sprites/detail-awning-wide.png +0 -0
- package/city/web/assets/sprites/detail-awning.png +0 -0
- package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
- package/city/web/assets/sprites/detail-overhang.png +0 -0
- package/city/web/assets/sprites/detail-parasol-a.png +0 -0
- package/city/web/assets/sprites/detail-parasol-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-c.png +0 -0
- package/city/web/assets/sprites/low-detail-building-d.png +0 -0
- package/city/web/assets/sprites/low-detail-building-e.png +0 -0
- package/city/web/assets/sprites/low-detail-building-f.png +0 -0
- package/city/web/assets/sprites/low-detail-building-g.png +0 -0
- package/city/web/assets/sprites/low-detail-building-h.png +0 -0
- package/city/web/assets/sprites/low-detail-building-i.png +0 -0
- package/city/web/assets/sprites/low-detail-building-j.png +0 -0
- package/city/web/assets/sprites/low-detail-building-k.png +0 -0
- package/city/web/assets/sprites/low-detail-building-l.png +0 -0
- package/city/web/assets/sprites/low-detail-building-m.png +0 -0
- package/city/web/assets/sprites/low-detail-building-n.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/assets/sprites/medidas.json +248 -0
- package/city/web/dist/city.js +939 -0
- package/city/web/dist/index.html +294 -0
- package/city/web/dist/modelos/License.txt +28 -0
- package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/dist/modelos/building-a.glb +0 -0
- package/city/web/dist/modelos/building-b.glb +0 -0
- package/city/web/dist/modelos/building-c.glb +0 -0
- package/city/web/dist/modelos/building-d.glb +0 -0
- package/city/web/dist/modelos/building-e.glb +0 -0
- package/city/web/dist/modelos/building-f.glb +0 -0
- package/city/web/dist/modelos/building-g.glb +0 -0
- package/city/web/dist/modelos/building-h.glb +0 -0
- package/city/web/dist/sprites/building-a.png +0 -0
- package/city/web/dist/sprites/building-b.png +0 -0
- package/city/web/dist/sprites/building-c.png +0 -0
- package/city/web/dist/sprites/building-d.png +0 -0
- package/city/web/dist/sprites/building-e.png +0 -0
- package/city/web/dist/sprites/building-f.png +0 -0
- package/city/web/dist/sprites/building-g.png +0 -0
- package/city/web/dist/sprites/building-h.png +0 -0
- package/city/web/dist/sprites/building-i.png +0 -0
- package/city/web/dist/sprites/building-j.png +0 -0
- package/city/web/dist/sprites/building-k.png +0 -0
- package/city/web/dist/sprites/building-l.png +0 -0
- package/city/web/dist/sprites/building-m.png +0 -0
- package/city/web/dist/sprites/building-n.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
- package/city/web/dist/sprites/catalogo.json +47 -0
- package/city/web/dist/sprites/detail-awning-wide.png +0 -0
- package/city/web/dist/sprites/detail-awning.png +0 -0
- package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
- package/city/web/dist/sprites/detail-overhang.png +0 -0
- package/city/web/dist/sprites/detail-parasol-a.png +0 -0
- package/city/web/dist/sprites/detail-parasol-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-c.png +0 -0
- package/city/web/dist/sprites/low-detail-building-d.png +0 -0
- package/city/web/dist/sprites/low-detail-building-e.png +0 -0
- package/city/web/dist/sprites/low-detail-building-f.png +0 -0
- package/city/web/dist/sprites/low-detail-building-g.png +0 -0
- package/city/web/dist/sprites/low-detail-building-h.png +0 -0
- package/city/web/dist/sprites/low-detail-building-i.png +0 -0
- package/city/web/dist/sprites/low-detail-building-j.png +0 -0
- package/city/web/dist/sprites/low-detail-building-k.png +0 -0
- package/city/web/dist/sprites/low-detail-building-l.png +0 -0
- package/city/web/dist/sprites/low-detail-building-m.png +0 -0
- package/city/web/dist/sprites/low-detail-building-n.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/dist/sprites/medidas.json +248 -0
- package/city/web/dist-hall/hall.js +1363 -0
- package/city/web/index.html +294 -0
- package/city/web/package.json +18 -0
- package/city/web/sella.py +21 -0
- package/city/web/src/activity-actors.ts +47 -0
- package/city/web/src/activity.ts +172 -0
- package/city/web/src/ayuntamiento.ts +580 -0
- package/city/web/src/draw.ts +640 -0
- package/city/web/src/game-speech.ts +79 -0
- package/city/web/src/hall.ts +1959 -0
- package/city/web/src/main.ts +2252 -0
- package/city/web/src/oven.ts +124 -0
- package/city/web/src/people.ts +244 -0
- package/city/web/src/presencia.ts +193 -0
- package/city/web/src/puertas.ts +180 -0
- package/city/web/src/repo-roles.ts +59 -0
- package/city/worker/package.json +16 -0
- package/city/worker/schema.sql +117 -0
- package/city/worker/src/index.ts +702 -0
- package/city/worker/src/square.ts +218 -0
- package/city/worker/wrangler.toml +58 -0
- package/demo/ada.md +50 -0
- package/demo/bruno.md +62 -0
- package/demo/camila.md +56 -0
- package/demo/city/ada.md +40 -0
- package/demo/city/city.yml +8 -0
- package/demo/city/parcels.yml +15 -0
- package/demo/city/roads.json +4 -0
- package/demo/city/units.yml +10 -0
- package/demo/clinica/city.yml +8 -0
- package/demo/clinica/parcels.yml +15 -0
- package/demo/clinica/roads.json +4 -0
- package/demo/clinica/units.yml +9 -0
- package/demo/clinica/vera.md +46 -0
- package/demo/dante.md +54 -0
- package/demo/despacho/city.yml +8 -0
- package/demo/despacho/marta.md +46 -0
- package/demo/despacho/parcels.yml +15 -0
- package/demo/despacho/roads.json +4 -0
- package/demo/despacho/units.yml +9 -0
- package/demo/elsa.md +50 -0
- package/demo/farid.md +52 -0
- package/demo/greta.md +50 -0
- package/demo/hugo.md +49 -0
- package/demo/iris.md +49 -0
- package/demo/jonas.md +51 -0
- package/demo/kira.md +51 -0
- package/demo/luca.md +49 -0
- package/demo/parcels.yml +92 -0
- package/demo/seed.py +20 -0
- package/demo/show.py +161 -0
- package/demo/stories.py +824 -0
- package/demo/units.yml +33 -0
- package/docs/agents-first.md +106 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +265 -0
- package/docs/self-host.md +151 -0
- package/docs/testing.md +205 -0
- package/package.json +73 -0
- package/plugin/.claude-plugin/plugin.json +62 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/channel/activity-cli.ts +16 -0
- package/plugin/channel/adapter-prompts.ts +113 -0
- package/plugin/channel/adapter.js +4292 -0
- package/plugin/channel/adapter.ts +133 -0
- package/plugin/channel/bus.js +20188 -0
- package/plugin/channel/bus.ts +131 -0
- package/plugin/channel/city-config.ts +125 -0
- package/plugin/channel/claude-channel.ts +58 -0
- package/plugin/channel/cli-args.ts +75 -0
- package/plugin/channel/client.js +4200 -0
- package/plugin/channel/client.ts +38 -0
- package/plugin/channel/committee/activity.ts +256 -0
- package/plugin/channel/committee/collection.ts +154 -0
- package/plugin/channel/committee/decision.ts +175 -0
- package/plugin/channel/committee/floor.ts +135 -0
- package/plugin/channel/committee/guards.ts +30 -0
- package/plugin/channel/committee/history.ts +38 -0
- package/plugin/channel/committee/render.ts +102 -0
- package/plugin/channel/committee/service.ts +72 -0
- package/plugin/channel/committee/storage.ts +92 -0
- package/plugin/channel/committee/types.ts +163 -0
- package/plugin/channel/committee/view.ts +63 -0
- package/plugin/channel/committee-cli.ts +109 -0
- package/plugin/channel/delivery-metrics.ts +61 -0
- package/plugin/channel/delivery-queue.ts +118 -0
- package/plugin/channel/hub/activity-controller.ts +72 -0
- package/plugin/channel/hub/activity-feed.ts +169 -0
- package/plugin/channel/hub/committee-controller.ts +60 -0
- package/plugin/channel/hub/connections.ts +48 -0
- package/plugin/channel/hub/diagnostics.ts +67 -0
- package/plugin/channel/hub/envelope-validity.ts +109 -0
- package/plugin/channel/hub/envelopes.ts +66 -0
- package/plugin/channel/hub/lifecycle.ts +91 -0
- package/plugin/channel/hub/local-roads.ts +89 -0
- package/plugin/channel/hub/remote-roads.ts +165 -0
- package/plugin/channel/hub/road-controller.ts +99 -0
- package/plugin/channel/hub-client.ts +149 -0
- package/plugin/channel/local-hub.js +5995 -0
- package/plugin/channel/local-hub.ts +349 -0
- package/plugin/channel/map-reporter.ts +48 -0
- package/plugin/channel/package-lock.json +1731 -0
- package/plugin/channel/package.json +19 -0
- package/plugin/channel/protocol.ts +98 -0
- package/plugin/channel/road-cli.ts +18 -0
- package/plugin/channel/run.sh +15 -0
- package/plugin/channel/runtime/claude.ts +458 -0
- package/plugin/channel/runtime/codex-config.ts +77 -0
- package/plugin/channel/runtime/codex.ts +669 -0
- package/plugin/channel/runtime/command.ts +62 -0
- package/plugin/channel/runtime/factory.ts +15 -0
- package/plugin/channel/runtime/json-rpc.ts +154 -0
- package/plugin/channel/runtime/kimi.ts +277 -0
- package/plugin/channel/runtime/opencode.ts +248 -0
- package/plugin/channel/runtime/process.ts +84 -0
- package/plugin/channel/runtime/types.ts +45 -0
- package/plugin/channel/runtime-files.ts +73 -0
- package/plugin/channel/runtime-gateway.js +6396 -0
- package/plugin/channel/runtime-gateway.ts +358 -0
- package/plugin/channel/runtime-metrics.ts +52 -0
- package/plugin/channel/runtime-subscription.ts +180 -0
- package/plugin/channel/terminal-delivery.ts +131 -0
- package/plugin/channel/untrusted.ts +60 -0
- package/plugin/commands/committee.md +31 -0
- package/plugin/commands/exit.md +32 -0
- package/plugin/commands/goals.md +20 -0
- package/plugin/commands/join.md +18 -0
- package/plugin/commands/notice.md +20 -0
- package/plugin/commands/propose.md +19 -0
- package/plugin/commands/round.md +17 -0
- package/plugin/commands/session.md +22 -0
- package/plugin/commands/settings.md +25 -0
- package/plugin/commands/setup.md +21 -0
- package/plugin/commands/team.md +23 -0
- package/plugin/domains/custom.md +27 -0
- package/plugin/domains/finance.md +31 -0
- package/plugin/domains/healthcare.md +34 -0
- package/plugin/domains/legal.md +31 -0
- package/plugin/domains/marketing.md +33 -0
- package/plugin/domains/operations.md +31 -0
- package/plugin/domains/research.md +32 -0
- package/plugin/domains/sales.md +30 -0
- package/plugin/domains/software.md +37 -0
- package/plugin/hooks/activity.sh +6 -0
- package/plugin/hooks/digging.sh +68 -0
- package/plugin/hooks/growth.sh +42 -0
- package/plugin/hooks/hooks.json +106 -0
- package/plugin/hooks/notice-on-pr.sh +35 -0
- package/plugin/hooks/notice-on-stop.sh +80 -0
- package/plugin/hooks/notice-pending.sh +58 -0
- package/plugin/hooks/solo-en-ciudad.sh +23 -0
- package/plugin/hooks/tokens.sh +47 -0
- package/plugin/roles/examples/account-executive.md +25 -0
- package/plugin/roles/examples/ai-manager.md +34 -0
- package/plugin/roles/examples/associate.md +24 -0
- package/plugin/roles/examples/brand-lead.md +20 -0
- package/plugin/roles/examples/cfo.md +20 -0
- package/plugin/roles/examples/city-lead.md +25 -0
- package/plugin/roles/examples/clinical-director.md +26 -0
- package/plugin/roles/examples/clinical-ops.md +25 -0
- package/plugin/roles/examples/clinician.md +25 -0
- package/plugin/roles/examples/compliance.md +27 -0
- package/plugin/roles/examples/content.md +24 -0
- package/plugin/roles/examples/controller.md +27 -0
- package/plugin/roles/examples/cpto.md +41 -0
- package/plugin/roles/examples/customer-success.md +25 -0
- package/plugin/roles/examples/data-engineer.md +37 -0
- package/plugin/roles/examples/data.md +60 -0
- package/plugin/roles/examples/dev.md +37 -0
- package/plugin/roles/examples/devops.md +39 -0
- package/plugin/roles/examples/enablement.md +24 -0
- package/plugin/roles/examples/ethics.md +24 -0
- package/plugin/roles/examples/fin-analytics.md +27 -0
- package/plugin/roles/examples/health-compliance.md +25 -0
- package/plugin/roles/examples/health-data.md +26 -0
- package/plugin/roles/examples/knowledge.md +25 -0
- package/plugin/roles/examples/lifecycle.md +26 -0
- package/plugin/roles/examples/llm-engineer.md +34 -0
- package/plugin/roles/examples/managing-partner.md +20 -0
- package/plugin/roles/examples/methods.md +23 -0
- package/plugin/roles/examples/operations-lead.md +23 -0
- package/plugin/roles/examples/ops.md +24 -0
- package/plugin/roles/examples/patient-safety.md +26 -0
- package/plugin/roles/examples/performance.md +27 -0
- package/plugin/roles/examples/po.md +36 -0
- package/plugin/roles/examples/process-owner.md +23 -0
- package/plugin/roles/examples/product-design.md +39 -0
- package/plugin/roles/examples/program-manager.md +24 -0
- package/plugin/roles/examples/quality.md +25 -0
- package/plugin/roles/examples/research-director.md +24 -0
- package/plugin/roles/examples/research-ops.md +25 -0
- package/plugin/roles/examples/researcher.md +25 -0
- package/plugin/roles/examples/revenue-lead.md +25 -0
- package/plugin/roles/examples/revops.md +26 -0
- package/plugin/roles/examples/seo.md +27 -0
- package/plugin/roles/examples/specialist.md +24 -0
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +492 -0
- package/plugin/scripts/capabilities.py +210 -0
- package/plugin/scripts/card.py +322 -0
- package/plugin/scripts/cities.py +520 -0
- package/plugin/scripts/city-env.sh +90 -0
- package/plugin/scripts/city-runtime.sh +87 -0
- package/plugin/scripts/city-session.sh +552 -0
- package/plugin/scripts/city_env.py +67 -0
- package/plugin/scripts/crecimiento.py +140 -0
- package/plugin/scripts/deliberations.py +48 -0
- package/plugin/scripts/doctor.py +294 -0
- package/plugin/scripts/domains.py +201 -0
- package/plugin/scripts/evidencia.py +64 -0
- package/plugin/scripts/find-repos.sh +111 -0
- package/plugin/scripts/gh.py +159 -0
- package/plugin/scripts/hall_protocol.py +147 -0
- package/plugin/scripts/hook_activity.py +139 -0
- package/plugin/scripts/launch.py +94 -0
- package/plugin/scripts/logs.py +98 -0
- package/plugin/scripts/pairing.py +185 -0
- package/plugin/scripts/parcels.py +132 -0
- package/plugin/scripts/read-card.py +55 -0
- package/plugin/scripts/report.py +201 -0
- package/plugin/scripts/reset.py +180 -0
- package/plugin/scripts/roads.py +230 -0
- package/plugin/scripts/roles.py +274 -0
- package/plugin/scripts/runtime_log.py +69 -0
- package/plugin/scripts/runtime_processes.py +132 -0
- package/plugin/scripts/rutas.py +83 -0
- package/plugin/scripts/seat.py +1696 -0
- package/plugin/scripts/tokens.py +194 -0
- package/plugin/scripts/trust-repos.py +59 -0
- package/plugin/scripts/ui.py +280 -0
- package/plugin/scripts/units.py +80 -0
- package/plugin/scripts/workspace.py +561 -0
- package/plugin/skills/city/SKILL.md +191 -0
- package/templates/blank.md +39 -0
- package/templates/finance.md +44 -0
- package/templates/legal.md +45 -0
- package/templates/marketing.md +48 -0
- package/templates/product.md +45 -0
package/bin/test-cage.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The cage: profile text everywhere, the real kernel where there is one.
|
|
3
|
+
|
|
4
|
+
The profile checks are pure and run on every platform, CI's Linux included.
|
|
5
|
+
The live checks exec `sandbox-exec` against a throwaway HOME with planted
|
|
6
|
+
fake secrets — they prove the four properties everything else relies on:
|
|
7
|
+
secrets unreadable, foreign writes refused, repo writes intact, grandchildren
|
|
8
|
+
still caged. On a machine without seatbelt they skip, loudly.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
|
|
17
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
18
|
+
RAIZ = os.path.dirname(AQUI)
|
|
19
|
+
sys.path.insert(0, os.path.join(RAIZ, "plugin", "scripts"))
|
|
20
|
+
sys.path.insert(0, AQUI)
|
|
21
|
+
|
|
22
|
+
import cage # noqa: E402
|
|
23
|
+
from testlib import afirma, comprueba, resumen # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def entorno_falso(como_un_home_real=False):
|
|
27
|
+
"""A throwaway HOME with credentials planted in it.
|
|
28
|
+
|
|
29
|
+
`como_un_home_real` puts it under the caller's own home instead of /tmp,
|
|
30
|
+
and that is not cosmetic: the Linux cage keeps /tmp writable, so a fixture
|
|
31
|
+
living there is writable through-and-through and CANNOT reproduce what a
|
|
32
|
+
real `/home/you` does. A launch-blocking bug shipped behind exactly that
|
|
33
|
+
blind spot — the suite was green while no real user could start a window.
|
|
34
|
+
"""
|
|
35
|
+
if como_un_home_real:
|
|
36
|
+
base = tempfile.mkdtemp(prefix=".agents-city-cage-", dir=os.path.expanduser("~"))
|
|
37
|
+
else:
|
|
38
|
+
base = tempfile.mkdtemp(prefix="agents-city-cage-")
|
|
39
|
+
casa = os.path.join(base, "home")
|
|
40
|
+
repo = os.path.join(base, "repo")
|
|
41
|
+
os.makedirs(os.path.join(casa, ".ssh"))
|
|
42
|
+
os.makedirs(repo)
|
|
43
|
+
with open(os.path.join(casa, ".ssh", "id_ed25519"), "w") as f:
|
|
44
|
+
f.write("FAKE-PRIVATE-KEY\n")
|
|
45
|
+
with open(os.path.join(casa, ".git-credentials"), "w") as f:
|
|
46
|
+
f.write("https://user:FAKE-TOKEN@github.com\n")
|
|
47
|
+
return base, casa, repo
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def texto_del_perfil():
|
|
51
|
+
base, casa, repo = entorno_falso()
|
|
52
|
+
try:
|
|
53
|
+
p = cage.perfil(repo, casa=casa)
|
|
54
|
+
afirma("the profile starts with the SBPL version header",
|
|
55
|
+
p.startswith("(version 1)"), p.splitlines()[0])
|
|
56
|
+
afirma("the profile allows by default and denies writes globally",
|
|
57
|
+
"(allow default)" in p and "(deny file-write*)" in p)
|
|
58
|
+
for secreto in (".ssh", ".aws", ".config/gcloud", ".config/gh"):
|
|
59
|
+
afirma(f"the profile seals {secreto} for reads and writes",
|
|
60
|
+
os.path.join(casa, *secreto.split("/")) in p, secreto)
|
|
61
|
+
afirma("the profile seals ~/.git-credentials",
|
|
62
|
+
os.path.join(casa, ".git-credentials") in p)
|
|
63
|
+
afirma("the repo itself is writable",
|
|
64
|
+
f'(subpath "{os.path.realpath(repo)}")' in p)
|
|
65
|
+
afirma("remote road .env files are sealed even inside ~/.claude",
|
|
66
|
+
"channels" in p and "\\.env" in p)
|
|
67
|
+
afirma("the broker state dir is sealed",
|
|
68
|
+
os.path.join(casa, ".agents-city", ".runtime", "broker") in p)
|
|
69
|
+
afirma("~/.npmrc stays readable on purpose (a broken npm protects nobody)",
|
|
70
|
+
os.path.join(casa, ".npmrc") not in p)
|
|
71
|
+
finally:
|
|
72
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def excepciones_y_errores():
|
|
76
|
+
base, casa, repo = entorno_falso()
|
|
77
|
+
try:
|
|
78
|
+
ficha = os.path.join(base, "win.token")
|
|
79
|
+
with open(ficha, "w") as f:
|
|
80
|
+
f.write("cb_x\n")
|
|
81
|
+
p = cage.perfil(repo, casa=casa, fichero_token=ficha)
|
|
82
|
+
afirma("the window's own broker token file is re-allowed read-only",
|
|
83
|
+
f'(allow file-read* (literal "{os.path.realpath(ficha)}"))' in p)
|
|
84
|
+
try:
|
|
85
|
+
cage.perfil(os.path.join(base, "no-such-dir"), casa=casa)
|
|
86
|
+
afirma("a nonexistent repo is refused", False)
|
|
87
|
+
except ValueError:
|
|
88
|
+
afirma("a nonexistent repo is refused", True)
|
|
89
|
+
previo = os.environ.get("CITY_CAGE")
|
|
90
|
+
os.environ["CITY_CAGE"] = "0"
|
|
91
|
+
comprueba("CITY_CAGE=0 launches uncaged, exactly the old behaviour",
|
|
92
|
+
cage.linea(repo, "w", casa=casa), "")
|
|
93
|
+
if previo is None:
|
|
94
|
+
os.environ.pop("CITY_CAGE")
|
|
95
|
+
else:
|
|
96
|
+
os.environ["CITY_CAGE"] = previo
|
|
97
|
+
os.environ["CITY_CAGE_DENY"] = os.path.join(casa, ".npmrc")
|
|
98
|
+
try:
|
|
99
|
+
afirma("CITY_CAGE_DENY adds an owner-chosen path to the seal",
|
|
100
|
+
os.path.join(casa, ".npmrc") in cage.perfil(repo, casa=casa))
|
|
101
|
+
finally:
|
|
102
|
+
os.environ.pop("CITY_CAGE_DENY")
|
|
103
|
+
# A repo that IS a sealed root (or covers one) is refused outright:
|
|
104
|
+
# there is no safe cage for a window rooted on the credential store.
|
|
105
|
+
try:
|
|
106
|
+
cage.perfil(os.path.join(casa, ".ssh"), casa=casa)
|
|
107
|
+
afirma("a repo path that is a sealed root is refused", False)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
afirma("a repo path that is a sealed root is refused", "unsafe" in str(e))
|
|
110
|
+
# An owner CITY_CAGE_ALLOW_WRITE that lands inside a sealed root is
|
|
111
|
+
# dropped (dead under the final deny) rather than emitted.
|
|
112
|
+
os.environ["CITY_CAGE_ALLOW_WRITE"] = os.path.join(casa, ".ssh", "sub")
|
|
113
|
+
try:
|
|
114
|
+
afirma("a write-allow inside a sealed root is not emitted",
|
|
115
|
+
os.path.join(casa, ".ssh", "sub") not in cage.perfil(repo, casa=casa))
|
|
116
|
+
finally:
|
|
117
|
+
os.environ.pop("CITY_CAGE_ALLOW_WRITE")
|
|
118
|
+
# A broad system root that merely COVERS a sealed root stays allowed:
|
|
119
|
+
# the final deny re-seals the secret, so writability is not sacrificed.
|
|
120
|
+
afirma("a covering system root is still allowed (ordering re-seals)",
|
|
121
|
+
'(subpath "/private/tmp")' in cage.perfil(repo, casa=casa))
|
|
122
|
+
# A relocated AGENTS_CITY_HOME must move the broker seal with it — the
|
|
123
|
+
# broker writes tokens there, so the cage must seal the same root.
|
|
124
|
+
relocado = os.path.join(base, "elsewhere-home")
|
|
125
|
+
os.makedirs(relocado)
|
|
126
|
+
previo_ac = os.environ.get("AGENTS_CITY_HOME")
|
|
127
|
+
os.environ["AGENTS_CITY_HOME"] = relocado
|
|
128
|
+
try:
|
|
129
|
+
p = cage.perfil(repo, casa=casa)
|
|
130
|
+
afirma("the broker seal follows a relocated AGENTS_CITY_HOME",
|
|
131
|
+
os.path.join(os.path.realpath(relocado), ".runtime", "broker") in p)
|
|
132
|
+
afirma("and the relocated home stays writable",
|
|
133
|
+
f'(subpath "{os.path.realpath(relocado)}")' in p)
|
|
134
|
+
finally:
|
|
135
|
+
if previo_ac is None:
|
|
136
|
+
os.environ.pop("AGENTS_CITY_HOME")
|
|
137
|
+
else:
|
|
138
|
+
os.environ["AGENTS_CITY_HOME"] = previo_ac
|
|
139
|
+
finally:
|
|
140
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def mounts_agente_primero():
|
|
144
|
+
# Agent-first: a workspace with a mount to a document folder outside it.
|
|
145
|
+
# The mount's resolved target must be writable, and secrets still sealed.
|
|
146
|
+
base, casa, _repo = entorno_falso()
|
|
147
|
+
try:
|
|
148
|
+
ws = os.path.join(base, "workspace")
|
|
149
|
+
docs = os.path.join(base, "elsewhere-docs")
|
|
150
|
+
os.makedirs(ws)
|
|
151
|
+
os.makedirs(docs)
|
|
152
|
+
p = cage.perfil(ws, casa=casa, extra_escritura=[docs])
|
|
153
|
+
afirma("a mount target is added as a writable root",
|
|
154
|
+
f'(subpath "{os.path.realpath(docs)}")' in p)
|
|
155
|
+
afirma("the workspace itself is writable",
|
|
156
|
+
f'(subpath "{os.path.realpath(ws)}")' in p)
|
|
157
|
+
# A mount that resolves inside a sealed root is refused, not honoured.
|
|
158
|
+
p2 = cage.perfil(ws, casa=casa, extra_escritura=[os.path.join(casa, ".ssh")])
|
|
159
|
+
afirma("a mount inside a sealed root is not made writable",
|
|
160
|
+
f'(subpath "{os.path.join(os.path.realpath(casa), ".ssh")}")'
|
|
161
|
+
not in p2.split("(deny file-read")[0])
|
|
162
|
+
finally:
|
|
163
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _enjaulado(perfil_ruta, *orden):
|
|
167
|
+
return subprocess.run(["sandbox-exec", "-f", perfil_ruta, *orden],
|
|
168
|
+
capture_output=True, text=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def jaula_viva():
|
|
172
|
+
if sys.platform != "darwin" or not shutil.which("sandbox-exec"):
|
|
173
|
+
print(" (live seatbelt checks skipped: no sandbox-exec on this machine)")
|
|
174
|
+
return
|
|
175
|
+
base, casa, repo = entorno_falso()
|
|
176
|
+
try:
|
|
177
|
+
ruta = os.path.join(base, "cage.sb")
|
|
178
|
+
with open(ruta, "w") as f:
|
|
179
|
+
f.write(cage.perfil(repo, casa=casa))
|
|
180
|
+
r = _enjaulado(ruta, "/bin/cat", os.path.join(casa, ".ssh", "id_ed25519"))
|
|
181
|
+
afirma("live: the planted SSH key is unreadable inside the cage",
|
|
182
|
+
r.returncode != 0 and "FAKE-PRIVATE-KEY" not in r.stdout, r.stdout or r.stderr)
|
|
183
|
+
r = _enjaulado(ruta, "/bin/cat", os.path.join(casa, ".git-credentials"))
|
|
184
|
+
afirma("live: the planted git credentials are unreadable inside the cage",
|
|
185
|
+
r.returncode != 0 and "FAKE-TOKEN" not in r.stdout)
|
|
186
|
+
r = _enjaulado(ruta, "/bin/zsh", "-c",
|
|
187
|
+
f"echo ok > {repo}/build.txt && cat {repo}/build.txt")
|
|
188
|
+
afirma("live: the window still writes freely in its own repo",
|
|
189
|
+
r.returncode == 0 and r.stdout.strip() == "ok", r.stderr)
|
|
190
|
+
# The throwaway HOME sits under the system TMPDIR, which the cage keeps
|
|
191
|
+
# writable on purpose — so the write-refusal proof targets a sealed dir.
|
|
192
|
+
r = _enjaulado(ruta, "/bin/zsh", "-c", f"echo leak > {casa}/.ssh/stolen.txt")
|
|
193
|
+
afirma("live: a write into a sealed directory is refused by the kernel",
|
|
194
|
+
r.returncode != 0
|
|
195
|
+
and not os.path.exists(os.path.join(casa, ".ssh", "stolen.txt")))
|
|
196
|
+
r = _enjaulado(ruta, "/bin/zsh", "-c",
|
|
197
|
+
f"/bin/zsh -c 'cat {casa}/.ssh/id_ed25519'")
|
|
198
|
+
afirma("live: a grandchild process inherits the cage",
|
|
199
|
+
r.returncode != 0 and "FAKE-PRIVATE-KEY" not in r.stdout)
|
|
200
|
+
linea = cage.linea(repo, "win one", casa=casa)
|
|
201
|
+
afirma("live: linea() hands back a sandbox-exec prefix",
|
|
202
|
+
linea.startswith("sandbox-exec -f "), linea)
|
|
203
|
+
perfil_generado = linea.split(" -f ", 1)[1].strip()
|
|
204
|
+
esperado = os.path.join(os.path.realpath(casa), ".agents-city", ".runtime", "cage")
|
|
205
|
+
afirma("live: the generated profile lands under the runtime cage dir with 0600",
|
|
206
|
+
perfil_generado.startswith(esperado)
|
|
207
|
+
and oct(os.stat(perfil_generado).st_mode & 0o777) == "0o600",
|
|
208
|
+
perfil_generado)
|
|
209
|
+
finally:
|
|
210
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def argv_de_linux():
|
|
214
|
+
"""The Linux argv, checked on whatever machine runs this.
|
|
215
|
+
|
|
216
|
+
The live namespace checks below only run on Linux, which means a macOS-only
|
|
217
|
+
contributor could break the Linux cage and see every suite pass — the shape
|
|
218
|
+
of bug this repo has already met once. The ORDER is the security invariant
|
|
219
|
+
(last mount wins), so the order is asserted everywhere.
|
|
220
|
+
"""
|
|
221
|
+
print(" the Linux cage's argv, from whichever kernel we are on")
|
|
222
|
+
base, casa, repo = entorno_falso()
|
|
223
|
+
try:
|
|
224
|
+
# Every path in the argv is canonical (on macOS /var resolves under
|
|
225
|
+
# /private), so the expectations resolve the same way the cage does.
|
|
226
|
+
real = cage.rutas.canonicaliza
|
|
227
|
+
argv = cage.argv_bwrap(repo, casa=casa)
|
|
228
|
+
texto = " ".join(argv)
|
|
229
|
+
afirma("it starts by binding the whole filesystem read-only",
|
|
230
|
+
argv[:5] == ["bwrap", "--ro-bind", "/", "/", "--dev"], " ".join(argv[:6]))
|
|
231
|
+
i_repo = argv.index(real(repo))
|
|
232
|
+
i_ssh = argv.index(real(os.path.join(casa, ".ssh")))
|
|
233
|
+
afirma("the writable working set is bound before the seals are applied",
|
|
234
|
+
i_repo < i_ssh, f"repo at {i_repo}, sealed .ssh at {i_ssh}")
|
|
235
|
+
afirma("a sealed directory becomes an empty tmpfs, not a refusal",
|
|
236
|
+
f"--tmpfs {real(os.path.join(casa, '.ssh'))}" in texto, texto[-400:])
|
|
237
|
+
afirma("a sealed FILE reads as nothing instead",
|
|
238
|
+
f"--ro-bind-try /dev/null {real(os.path.join(casa, '.git-credentials'))}" in texto,
|
|
239
|
+
texto[-400:])
|
|
240
|
+
afirma("the window keeps its own repo writable",
|
|
241
|
+
f"--bind-try {real(repo)} {real(repo)}" in texto, texto[:400])
|
|
242
|
+
# NOT --die-with-parent, and not by omission: the bus hub is started
|
|
243
|
+
# detached on purpose, so tying the namespace to one pane would take
|
|
244
|
+
# the city's bus down with whichever window happened to start it.
|
|
245
|
+
afirma("the namespace does not tie itself to one pane's lifetime",
|
|
246
|
+
"--die-with-parent" not in argv)
|
|
247
|
+
# /proc arrives with the read-only bind of /. Mounting a fresh one
|
|
248
|
+
# without a PID namespace buys nothing, and WITH one it would make the
|
|
249
|
+
# gateway record a namespace-local pid that `agents-city exit` then
|
|
250
|
+
# signals on the host.
|
|
251
|
+
afirma("no separate /proc mount, so recorded pids stay host pids",
|
|
252
|
+
"--proc" not in argv)
|
|
253
|
+
afirma("the device tree is the sandbox's own, not the host's re-bound",
|
|
254
|
+
argv.count("/dev") == 1, " ".join(a for a in argv if "dev" in a))
|
|
255
|
+
afirma("the probe runs exactly the flags a real launch starts from",
|
|
256
|
+
argv[:len(cage.BASE_BWRAP)] == cage.BASE_BWRAP, " ".join(argv[:8]))
|
|
257
|
+
afirma("Claude's own config file at the HOME root stays writable",
|
|
258
|
+
f"--bind-try {real(os.path.join(casa, '.claude.json'))}" in texto,
|
|
259
|
+
texto[-300:])
|
|
260
|
+
# The broker token is the one file re-admitted, and it must come after
|
|
261
|
+
# the seal that hides the directory it lives in — last mount wins.
|
|
262
|
+
token = os.path.join(casa, ".agents-city", ".runtime", "broker", "web.token")
|
|
263
|
+
os.makedirs(os.path.dirname(token), exist_ok=True)
|
|
264
|
+
open(token, "w").write("t")
|
|
265
|
+
conficha = cage.argv_bwrap(repo, casa=casa, fichero_token=token)
|
|
266
|
+
i_sello = max(i for i, a in enumerate(conficha)
|
|
267
|
+
if a.endswith(os.path.join(".runtime", "broker")))
|
|
268
|
+
i_token = max(i for i, a in enumerate(conficha) if a == real(token))
|
|
269
|
+
afirma("this window's own token is re-admitted after the broker seal",
|
|
270
|
+
i_token > i_sello, f"seal at {i_sello}, token at {i_token}")
|
|
271
|
+
# A mount that resolves inside a sealed root must never become writable.
|
|
272
|
+
dentro = os.path.join(casa, ".ssh", "robado")
|
|
273
|
+
os.makedirs(dentro, exist_ok=True)
|
|
274
|
+
con_fuga = " ".join(cage.argv_bwrap(repo, casa=casa, extra_escritura=(dentro,)))
|
|
275
|
+
afirma("a mount inside a sealed root is dropped, not bound writable",
|
|
276
|
+
f"--bind-try {real(dentro)}" not in con_fuga, con_fuga[-300:])
|
|
277
|
+
finally:
|
|
278
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def sonda_compartida():
|
|
282
|
+
"""The bubblewrap probe is paid once per city, not once per window.
|
|
283
|
+
|
|
284
|
+
`cage.py` runs as a fresh process per window, so an in-process memo dies
|
|
285
|
+
every time: without a channel between them, a city of eight agents forks
|
|
286
|
+
eight namespaces before the first prompt — and on a kernel that refuses
|
|
287
|
+
them, forks eight failures.
|
|
288
|
+
"""
|
|
289
|
+
print(" the probe that must not be paid per window")
|
|
290
|
+
previo = os.environ.get("CITY_CAGE_BWRAP")
|
|
291
|
+
try:
|
|
292
|
+
os.environ["CITY_CAGE_BWRAP"] = "0"
|
|
293
|
+
afirma("an owner-supplied answer is believed without probing",
|
|
294
|
+
cage.bwrap_sirve() is False)
|
|
295
|
+
os.environ["CITY_CAGE_BWRAP"] = "1"
|
|
296
|
+
# Believed, but never blindly: tmux windows inherit the server's whole
|
|
297
|
+
# environment, so a value carried over from another machine would build
|
|
298
|
+
# a prefix that exits 127 on every window. The binary still has to be
|
|
299
|
+
# there — which is why this asserts agreement with `which`, not True.
|
|
300
|
+
afirma("an affirmative answer still requires bwrap to exist here",
|
|
301
|
+
cage.bwrap_sirve() == (shutil.which("bwrap") is not None))
|
|
302
|
+
finally:
|
|
303
|
+
if previo is None:
|
|
304
|
+
os.environ.pop("CITY_CAGE_BWRAP", None)
|
|
305
|
+
else:
|
|
306
|
+
os.environ["CITY_CAGE_BWRAP"] = previo
|
|
307
|
+
lanzador = open(os.path.join(RAIZ, "plugin", "scripts", "city-session.sh"),
|
|
308
|
+
encoding="utf-8").read()
|
|
309
|
+
afirma("the launcher asks once and exports the answer to every window",
|
|
310
|
+
"export CITY_CAGE_BWRAP" in lanzador and lanzador.count("$CAGE\" check") <= 1,
|
|
311
|
+
"")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def jaula_linux():
|
|
315
|
+
"""The Linux cage, exercised against a real namespace.
|
|
316
|
+
|
|
317
|
+
Same guarantees as the macOS block above, proven the same way: a planted
|
|
318
|
+
key must be unreadable, the repo must stay writable, and a grandchild must
|
|
319
|
+
not escape. Different kernel, identical promise — which is the only reason
|
|
320
|
+
it is allowed to be a different mechanism.
|
|
321
|
+
"""
|
|
322
|
+
if not sys.platform.startswith("linux"):
|
|
323
|
+
print(" (live bubblewrap checks skipped: not Linux)")
|
|
324
|
+
return
|
|
325
|
+
if not cage.bwrap_sirve():
|
|
326
|
+
if os.environ.get("CITY_CAGE_REQUIRED") == "1":
|
|
327
|
+
afirma("the Linux cage is available where it was required", False,
|
|
328
|
+
"bwrap missing or namespaces refused, and CITY_CAGE_REQUIRED=1")
|
|
329
|
+
return
|
|
330
|
+
print(" (live bubblewrap checks skipped: bwrap missing or namespaces refused)")
|
|
331
|
+
return
|
|
332
|
+
base, casa, repo = entorno_falso(como_un_home_real=True)
|
|
333
|
+
try:
|
|
334
|
+
def dentro(*orden):
|
|
335
|
+
argv = cage.argv_bwrap(repo, casa=casa)
|
|
336
|
+
return subprocess.run([*argv, *orden], capture_output=True, text=True)
|
|
337
|
+
|
|
338
|
+
# First: the cage must START. Four of the checks below are vacuous if
|
|
339
|
+
# bwrap dies — "the key was unreadable" is also true when nothing ran.
|
|
340
|
+
arranque = dentro("/bin/echo", "in")
|
|
341
|
+
afirma("live: a window actually launches inside the cage",
|
|
342
|
+
arranque.returncode == 0 and arranque.stdout.strip() == "in",
|
|
343
|
+
arranque.stderr or arranque.stdout)
|
|
344
|
+
r = dentro("/bin/cat", os.path.join(casa, ".ssh", "id_ed25519"))
|
|
345
|
+
afirma("live: the planted SSH key is unreadable inside the cage",
|
|
346
|
+
r.returncode != 0 and "FAKE-PRIVATE-KEY" not in r.stdout, r.stdout or r.stderr)
|
|
347
|
+
r = dentro("/bin/cat", os.path.join(casa, ".git-credentials"))
|
|
348
|
+
afirma("live: the planted git credentials are unreachable inside the cage",
|
|
349
|
+
r.returncode != 0 and "FAKE-TOKEN" not in r.stdout, r.stdout or r.stderr)
|
|
350
|
+
r = dentro("/bin/sh", "-c", f"echo ok > {repo}/build.txt && cat {repo}/build.txt")
|
|
351
|
+
afirma("live: the window still writes freely in its own repo",
|
|
352
|
+
r.returncode == 0 and r.stdout.strip() == "ok", r.stderr)
|
|
353
|
+
fuga = f"echo leak > {casa}/.ssh/stolen.txt; cat {casa}/.ssh/stolen.txt"
|
|
354
|
+
r = dentro("/bin/sh", "-c", fuga)
|
|
355
|
+
afirma("live: a write into a sealed directory never reaches the real disk",
|
|
356
|
+
not os.path.exists(os.path.join(casa, ".ssh", "stolen.txt")), r.stdout or r.stderr)
|
|
357
|
+
r = dentro("/bin/sh", "-c", f"/bin/sh -c 'cat {casa}/.ssh/id_ed25519'")
|
|
358
|
+
afirma("live: a grandchild process inherits the cage",
|
|
359
|
+
r.returncode != 0 and "FAKE-PRIVATE-KEY" not in r.stdout)
|
|
360
|
+
linea = cage.linea(repo, "win one", casa=casa)
|
|
361
|
+
afirma("live: linea() hands back a bwrap prefix on Linux",
|
|
362
|
+
linea.startswith("bwrap ") and linea.endswith(" "), linea[:80])
|
|
363
|
+
finally:
|
|
364
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
texto_del_perfil()
|
|
368
|
+
excepciones_y_errores()
|
|
369
|
+
mounts_agente_primero()
|
|
370
|
+
jaula_viva()
|
|
371
|
+
argv_de_linux()
|
|
372
|
+
sonda_compartida()
|
|
373
|
+
jaula_linux()
|
|
374
|
+
sys.exit(resumen("cage"))
|