agents-city 0.3.0-beta.21 → 0.3.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 +1 -1
- package/README.es.md +310 -70
- package/README.md +297 -69
- package/bin/agents-city.js +3 -0
- package/bin/doctor +3 -0
- package/bin/hall.html +164 -24
- package/bin/navegador.mjs +415 -0
- package/bin/serve.py +383 -127
- package/bin/shortcut +3 -0
- package/bin/test +5 -2
- package/bin/test-actualiza.py +130 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-busca.py +216 -0
- package/bin/test-cage.py +170 -2
- package/bin/test-card.py +2 -2
- package/bin/test-cities.py +45 -0
- package/bin/test-contracts.py +12 -5
- package/bin/test-doctor.py +33 -0
- package/bin/test-navegador.py +164 -0
- package/bin/test-seat.py +245 -25
- package/bin/test-serve.py +214 -9
- package/bin/test-workspace.py +63 -0
- package/bin/testlib.py +23 -0
- package/bin/update +3 -0
- package/city/web/dist/city.js +47 -47
- package/city/web/dist/index.html +1 -1
- package/city/web/dist-hall/hall.js +2193 -174
- package/city/web/src/bienvenida.ts +686 -0
- package/city/web/src/es.ts +180 -0
- package/city/web/src/hall.ts +520 -168
- package/city/web/src/idioma.ts +86 -0
- package/city/web/src/main.ts +27 -0
- package/city/web/src/motores.ts +54 -0
- package/docs/agents-first.md +8 -1
- package/docs/security.md +46 -12
- package/docs/testing.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/busca.py +436 -0
- package/plugin/scripts/cage.py +266 -26
- package/plugin/scripts/capabilities.py +17 -10
- package/plugin/scripts/card.py +10 -0
- package/plugin/scripts/cities.py +34 -0
- package/plugin/scripts/city-session.sh +33 -7
- package/plugin/scripts/doctor.py +122 -0
- package/plugin/scripts/find-repos.sh +12 -105
- package/plugin/scripts/read-card.py +6 -2
- package/plugin/scripts/report.py +5 -6
- package/plugin/scripts/reset.py +50 -14
- package/plugin/scripts/seat.py +445 -103
- package/plugin/scripts/workspace.py +197 -0
package/bin/test-serve.py
CHANGED
|
@@ -169,6 +169,53 @@ def main():
|
|
|
169
169
|
and b".liveAvatar" in cuerpo,
|
|
170
170
|
cuerpo[:500].decode(errors="replace"),
|
|
171
171
|
)
|
|
172
|
+
# Two skins, one set of names. A colour written straight into a rule is
|
|
173
|
+
# a colour that exists in one theme only — the classic unreadable-page
|
|
174
|
+
# bug — so the palette is asserted to be the only place naming one.
|
|
175
|
+
pagina = cuerpo.decode(errors="replace")
|
|
176
|
+
cabecera = pagina.split("*{box-sizing", 1)[0]
|
|
177
|
+
cuerpo_css = pagina.split("*{box-sizing", 1)[1].split("</style>", 1)[0]
|
|
178
|
+
afirma(
|
|
179
|
+
"· daylight is the default skin, in the tokens of agentscity.net",
|
|
180
|
+
"--suelo:#f3f0e8" in cabecera
|
|
181
|
+
and "--tinta:#1d2229" in cabecera
|
|
182
|
+
and "--lampara:#7048e8" in cabecera,
|
|
183
|
+
cabecera[-500:],
|
|
184
|
+
)
|
|
185
|
+
afirma(
|
|
186
|
+
"· night is declared once and only mapped, never written twice",
|
|
187
|
+
"--noche-suelo:#131a24" in cabecera
|
|
188
|
+
and cabecera.count("--noche-suelo:#131a24") == 1,
|
|
189
|
+
cabecera[-500:],
|
|
190
|
+
)
|
|
191
|
+
afirma(
|
|
192
|
+
# Daylight is THE default, not a default a dark desktop overrides:
|
|
193
|
+
# the owner asked for light and a machine setting does not outvote
|
|
194
|
+
# them. Night is reachable only through the switch, and remembered.
|
|
195
|
+
"· night is a choice made here, never one the machine makes",
|
|
196
|
+
'[data-tema="oscuro"]' in cabecera
|
|
197
|
+
# The RULE, not the word: the palette block explains in prose
|
|
198
|
+
# that there deliberately is no media query, and a test that
|
|
199
|
+
# cannot tell a comment from a rule fails on its own docs.
|
|
200
|
+
and "@media(prefers-color-scheme" not in cabecera,
|
|
201
|
+
cabecera[-700:],
|
|
202
|
+
)
|
|
203
|
+
import re as _re
|
|
204
|
+
|
|
205
|
+
sueltos = [
|
|
206
|
+
c for c in _re.findall(r"#[0-9a-fA-F]{6}\b", cuerpo_css)
|
|
207
|
+
# The kind chips are brand identity, the same hue in both skins.
|
|
208
|
+
if c.lower() not in ("#3b82f6", "#8b5cf6", "#f59e0b", "#0f172a", "#f4efe5")
|
|
209
|
+
]
|
|
210
|
+
afirma(
|
|
211
|
+
"· and no rule outside the palette hardcodes a colour of its own",
|
|
212
|
+
not sueltos, ", ".join(sorted(set(sueltos))),
|
|
213
|
+
)
|
|
214
|
+
afirma(
|
|
215
|
+
"· the switch is in the page, and says which skin is showing",
|
|
216
|
+
'id="temaBoton"' in pagina, "",
|
|
217
|
+
)
|
|
218
|
+
|
|
172
219
|
map_html = open(os.path.join(RAIZ, "city", "web", "index.html")).read()
|
|
173
220
|
hall_source = open(os.path.join(RAIZ, "city", "web", "src", "hall.ts")).read()
|
|
174
221
|
speech_source = open(os.path.join(RAIZ, "city", "web", "src", "game-speech.ts")).read()
|
|
@@ -283,6 +330,10 @@ def main():
|
|
|
283
330
|
)
|
|
284
331
|
|
|
285
332
|
# A seat written through the hall is the same card as everywhere else.
|
|
333
|
+
# The roster is NOT part of it: agents are added through /api/agentes,
|
|
334
|
+
# and saving the seat must carry them over untouched — the whole point
|
|
335
|
+
# of having one writer per fact. The card comes first: an agent lives on
|
|
336
|
+
# its owner's card, so there is nothing to join until that exists.
|
|
286
337
|
st, cuerpo = pide(
|
|
287
338
|
puerto,
|
|
288
339
|
"/api/ficha",
|
|
@@ -291,8 +342,6 @@ def main():
|
|
|
291
342
|
"user": "Ana.López@x.com",
|
|
292
343
|
"domain": "healthcare",
|
|
293
344
|
"role": "clinical-director",
|
|
294
|
-
"repos": ["api"],
|
|
295
|
-
"repo_roles": {"api": "seo"},
|
|
296
345
|
"objetivo": {
|
|
297
346
|
"title": "One goal",
|
|
298
347
|
"signal": "s",
|
|
@@ -306,11 +355,33 @@ def main():
|
|
|
306
355
|
r = json.loads(cuerpo)
|
|
307
356
|
comprueba("· a card is written through the hall", st, 200)
|
|
308
357
|
comprueba("· body.user cannot create a second person in the city", r["user"], owner)
|
|
358
|
+
st_alta, cuerpo_alta = pide(
|
|
359
|
+
puerto, "/api/agentes", metodo="POST",
|
|
360
|
+
cuerpo={"name": "api", "kind": "code", "role": "seo"},
|
|
361
|
+
)
|
|
362
|
+
comprueba("· an agent joins the city through its own endpoint", st_alta, 200)
|
|
363
|
+
# Saving the seat a second time is where the old shape would have
|
|
364
|
+
# silently replaced the roster with whatever the page happened to send.
|
|
365
|
+
pide(
|
|
366
|
+
puerto, "/api/ficha", metodo="POST",
|
|
367
|
+
cuerpo={"domain": "healthcare", "role": "clinical-director"},
|
|
368
|
+
)
|
|
309
369
|
c = card.lee(os.path.join(hallDatos, f"{owner}.md"))
|
|
370
|
+
texto_ficha = c.get("texto") or ""
|
|
310
371
|
comprueba(
|
|
311
372
|
"· and reads back through the shared reader",
|
|
312
|
-
(c["agent"], c["role"]
|
|
313
|
-
(address, "clinical-director"
|
|
373
|
+
(c["agent"], c["role"]),
|
|
374
|
+
(address, "clinical-director"),
|
|
375
|
+
)
|
|
376
|
+
afirma(
|
|
377
|
+
"· saving the seat carries the roster over instead of emptying the city",
|
|
378
|
+
"agents: [api]" in texto_ficha and "role.api: seo" in texto_ficha,
|
|
379
|
+
texto_ficha.split("---")[1],
|
|
380
|
+
)
|
|
381
|
+
afirma(
|
|
382
|
+
"· and nothing writes the legacy repos: shape any more",
|
|
383
|
+
"repos:" not in texto_ficha,
|
|
384
|
+
texto_ficha.split("---")[1],
|
|
314
385
|
)
|
|
315
386
|
estado_salud = json.loads(pide(puerto, "/api/estado")[1])
|
|
316
387
|
afirma(
|
|
@@ -334,6 +405,9 @@ def main():
|
|
|
334
405
|
"· an empty command is manual, not an empty string to run", c["objetivo"]["command"], ""
|
|
335
406
|
)
|
|
336
407
|
|
|
408
|
+
# The seat endpoint no longer takes a roster in any shape: sending one
|
|
409
|
+
# is ignored rather than obeyed, because agents have their own door and
|
|
410
|
+
# a second way in is how the two doors drifted apart in the first place.
|
|
337
411
|
st, cuerpo = pide(
|
|
338
412
|
puerto,
|
|
339
413
|
"/api/ficha",
|
|
@@ -341,14 +415,15 @@ def main():
|
|
|
341
415
|
cuerpo={
|
|
342
416
|
"domain": "healthcare",
|
|
343
417
|
"role": "clinical-director",
|
|
344
|
-
"repos": ["
|
|
345
|
-
"repo_roles":
|
|
418
|
+
"repos": ["inventado"],
|
|
419
|
+
"repo_roles": {"inventado": "seo"},
|
|
346
420
|
},
|
|
347
421
|
)
|
|
422
|
+
texto_tras = card.lee(os.path.join(hallDatos, f"{owner}.md")).get("texto") or ""
|
|
348
423
|
afirma(
|
|
349
|
-
"·
|
|
350
|
-
st ==
|
|
351
|
-
|
|
424
|
+
"· a roster sent to the seat endpoint is ignored, not written",
|
|
425
|
+
st == 200 and "inventado" not in texto_tras and "agents: [api]" in texto_tras,
|
|
426
|
+
texto_tras.split("---")[1],
|
|
352
427
|
)
|
|
353
428
|
|
|
354
429
|
# A goal with no title is no goal, not a goal called nothing.
|
|
@@ -664,6 +739,92 @@ def main():
|
|
|
664
739
|
)
|
|
665
740
|
pide(puerto, "/api/agente", metodo="POST", cuerpo={"agent": "notas", "model": ""})
|
|
666
741
|
|
|
742
|
+
# ── the roster, from the web ─────────────────────────────────────────
|
|
743
|
+
print(" building the roster from the Hall")
|
|
744
|
+
st, cuerpo = pide(
|
|
745
|
+
puerto,
|
|
746
|
+
"/api/agentes",
|
|
747
|
+
metodo="POST",
|
|
748
|
+
cuerpo={"name": "urgencias web", "kind": "knowledge", "role": "triage"},
|
|
749
|
+
)
|
|
750
|
+
carta = open(os.path.join(hallDatos, "halltest.md"), encoding="utf-8").read()
|
|
751
|
+
afirma(
|
|
752
|
+
"· happy: an agent can be added without ever opening a terminal",
|
|
753
|
+
st == 200
|
|
754
|
+
and "urgencias web" in carta
|
|
755
|
+
and "kind.urgencias-web: knowledge" in carta
|
|
756
|
+
and "role.urgencias-web: triage" in carta,
|
|
757
|
+
carta,
|
|
758
|
+
)
|
|
759
|
+
st, _ = pide(
|
|
760
|
+
puerto, "/api/agentes", metodo="POST",
|
|
761
|
+
cuerpo={"name": "urgencias web", "kind": "code", "role": "dev"},
|
|
762
|
+
)
|
|
763
|
+
comprueba("· non-happy: two agents cannot share one identity", st, 409)
|
|
764
|
+
st, _ = pide(
|
|
765
|
+
puerto, "/api/agentes", metodo="POST",
|
|
766
|
+
cuerpo={"name": "nova", "kind": "wizard", "role": "dev"},
|
|
767
|
+
)
|
|
768
|
+
comprueba("· non-happy: a kind is code, knowledge or coordinator", st, 400)
|
|
769
|
+
st, _ = pide(
|
|
770
|
+
puerto, "/api/agentes", metodo="POST",
|
|
771
|
+
cuerpo={"name": "nova", "kind": "code", "role": "dev; rm -rf /"},
|
|
772
|
+
)
|
|
773
|
+
comprueba("· non-happy: a role is an id, never a shell fragment", st, 400)
|
|
774
|
+
st, _ = pide(puerto, "/api/agentes", metodo="POST", cuerpo={"name": " "})
|
|
775
|
+
comprueba("· non-happy: an agent needs a name", st, 400)
|
|
776
|
+
|
|
777
|
+
# Mounts: what an agent works on, from the same page.
|
|
778
|
+
carpeta = tempfile.mkdtemp()
|
|
779
|
+
st, cuerpo = pide(
|
|
780
|
+
puerto, "/api/montaje", metodo="POST",
|
|
781
|
+
cuerpo={"agent": "urgencias-web", "add": carpeta},
|
|
782
|
+
)
|
|
783
|
+
montado = json.loads(cuerpo)
|
|
784
|
+
# The label is slugged: it is a card key and a filename, not free text.
|
|
785
|
+
etiqueta = card.ventana(os.path.basename(carpeta))
|
|
786
|
+
carta = open(os.path.join(hallDatos, "halltest.md"), encoding="utf-8").read()
|
|
787
|
+
afirma(
|
|
788
|
+
"· happy: a document folder is mounted, on disk and on the card together",
|
|
789
|
+
st == 200
|
|
790
|
+
and any(m["label"] == etiqueta for m in montado.get("mounts", []))
|
|
791
|
+
and f"mounts.urgencias-web: [{os.path.realpath(carpeta)}]" in carta,
|
|
792
|
+
f"{cuerpo!r} {carta}",
|
|
793
|
+
)
|
|
794
|
+
st, cuerpo = pide(puerto, "/api/estado")
|
|
795
|
+
hoja_web = next(
|
|
796
|
+
x for x in json.loads(cuerpo)["agents"] if x["slug"] == "urgencias-web"
|
|
797
|
+
)
|
|
798
|
+
afirma(
|
|
799
|
+
"· and the sheet says what it works on, not merely how many",
|
|
800
|
+
[m["label"] for m in hoja_web["mounts"]] == [etiqueta],
|
|
801
|
+
str(hoja_web["mounts"]),
|
|
802
|
+
)
|
|
803
|
+
st, _ = pide(
|
|
804
|
+
puerto, "/api/montaje", metodo="POST",
|
|
805
|
+
cuerpo={"agent": "urgencias-web", "add": "/nope/not/here"},
|
|
806
|
+
)
|
|
807
|
+
comprueba("· non-happy: a folder that is not there is refused", st, 400)
|
|
808
|
+
st, cuerpo = pide(
|
|
809
|
+
puerto, "/api/montaje", metodo="POST",
|
|
810
|
+
cuerpo={"agent": "urgencias-web", "remove": etiqueta},
|
|
811
|
+
)
|
|
812
|
+
carta = open(os.path.join(hallDatos, "halltest.md"), encoding="utf-8").read()
|
|
813
|
+
afirma(
|
|
814
|
+
"· happy: unmounting clears the link and the card key, and keeps the folder",
|
|
815
|
+
st == 200
|
|
816
|
+
and not json.loads(cuerpo)["mounts"]
|
|
817
|
+
and "mounts.urgencias-web" not in carta
|
|
818
|
+
and os.path.isdir(carpeta),
|
|
819
|
+
carta,
|
|
820
|
+
)
|
|
821
|
+
st, _ = pide(
|
|
822
|
+
puerto, "/api/montaje", metodo="POST",
|
|
823
|
+
cuerpo={"agent": "urgencias-web", "remove": "fantasma"},
|
|
824
|
+
)
|
|
825
|
+
comprueba("· non-happy: unmounting what is not mounted is a 404", st, 404)
|
|
826
|
+
shutil.rmtree(carpeta, ignore_errors=True)
|
|
827
|
+
|
|
667
828
|
# ── instructions and skills, into the agent's own home ──────────────
|
|
668
829
|
print(" instructions and skills, written only into the agent's own home")
|
|
669
830
|
st, cuerpo = pide(puerto, "/api/instrucciones?agent=notas&file=CLAUDE.md")
|
|
@@ -1006,6 +1167,50 @@ def main():
|
|
|
1006
1167
|
if otro.get("s"):
|
|
1007
1168
|
otro["s"].server_close()
|
|
1008
1169
|
|
|
1170
|
+
# Two memos serve /api/estado, and a memo without an invalidation is a
|
|
1171
|
+
# lie waiting to be told: mounting a folder changes exactly what growth
|
|
1172
|
+
# counts, and installing a skill changes discovery without touching the
|
|
1173
|
+
# card that keys it. A healthy card first — the checks above deliberately
|
|
1174
|
+
# left a broken one behind.
|
|
1175
|
+
with open(os.path.join(hallDatos, "halltest.md"), "w", encoding="utf-8") as f:
|
|
1176
|
+
f.write(
|
|
1177
|
+
"---\nuser: halltest\nname: Hall Test\nrole: cpto\nagent: halltest/seat\n"
|
|
1178
|
+
"agents: [notas]\nkind.notas: knowledge\nrole.notas: apuntes\n"
|
|
1179
|
+
"goals_defined: true\n---\n"
|
|
1180
|
+
)
|
|
1181
|
+
serve._CRECIDO[(os.path.realpath(hallDatos), "notas")] = (
|
|
1182
|
+
__import__("time").monotonic(),
|
|
1183
|
+
{"floors": 999, "bricks": 0, "activity30": 0, "signal": "stale"},
|
|
1184
|
+
)
|
|
1185
|
+
pide(
|
|
1186
|
+
puerto, "/api/montaje", metodo="POST",
|
|
1187
|
+
cuerpo={"agent": "notas", "add": tempfile.mkdtemp()},
|
|
1188
|
+
)
|
|
1189
|
+
afirma(
|
|
1190
|
+
"· mounting a folder forgets that agent's remembered growth",
|
|
1191
|
+
(os.path.realpath(hallDatos), "notas") not in serve._CRECIDO,
|
|
1192
|
+
str(sorted(serve._CRECIDO)),
|
|
1193
|
+
)
|
|
1194
|
+
st, cuerpo = pide(puerto, "/api/estado")
|
|
1195
|
+
primera = json.loads(cuerpo)["skills"]
|
|
1196
|
+
sello_antes = serve._SKILLS.get("sello")
|
|
1197
|
+
pide(puerto, "/api/estado")
|
|
1198
|
+
afirma(
|
|
1199
|
+
"· discovery is remembered while the card has not moved",
|
|
1200
|
+
sello_antes is not None and serve._SKILLS.get("sello") == sello_antes,
|
|
1201
|
+
str(sello_antes),
|
|
1202
|
+
)
|
|
1203
|
+
pide(
|
|
1204
|
+
puerto, "/api/agentes", metodo="POST",
|
|
1205
|
+
cuerpo={"name": "efimero", "kind": "code", "role": "dev"},
|
|
1206
|
+
)
|
|
1207
|
+
st, cuerpo = pide(puerto, "/api/estado")
|
|
1208
|
+
afirma(
|
|
1209
|
+
"· and a card that moves invalidates it at once, not on a timer",
|
|
1210
|
+
"efimero" in json.loads(cuerpo)["skills"] and "efimero" not in primera,
|
|
1211
|
+
str(sorted(json.loads(cuerpo)["skills"])),
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1009
1214
|
# A code agent's growth reads real local git history — merge commits
|
|
1010
1215
|
# as floors — instead of answering zero for lack of a counter.
|
|
1011
1216
|
import subprocess as _sp
|
package/bin/test-workspace.py
CHANGED
|
@@ -179,8 +179,71 @@ def _rechaza(fn):
|
|
|
179
179
|
return True
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
def skills_instaladas():
|
|
183
|
+
"""The one deliberate write: a skill the owner hands over, into one home.
|
|
184
|
+
|
|
185
|
+
Both doors (the wizard's question, the Hall's upload) come through here, so
|
|
186
|
+
the containment rules are tested once — where they live.
|
|
187
|
+
"""
|
|
188
|
+
print(" installing a skill into one agent's own home")
|
|
189
|
+
base = tempfile.mkdtemp(prefix="agents-city-skill-")
|
|
190
|
+
try:
|
|
191
|
+
origen = os.path.join(base, "triage-skill")
|
|
192
|
+
os.makedirs(origen)
|
|
193
|
+
with open(os.path.join(origen, "SKILL.md"), "w") as f:
|
|
194
|
+
f.write("---\nname: triage\n---\n")
|
|
195
|
+
nombre, mal = ws.instala_skill(base, "urgencias", origen)
|
|
196
|
+
destino = os.path.join(ws.workspace_de(base, "urgencias"), ".claude", "skills",
|
|
197
|
+
"triage-skill", "SKILL.md")
|
|
198
|
+
afirma("· a folder with a SKILL.md installs into the agent's home",
|
|
199
|
+
nombre == "triage-skill" and not mal and os.path.isfile(destino), f"{nombre} {mal}")
|
|
200
|
+
_, otra_vez = ws.instala_skill(base, "urgencias", origen)
|
|
201
|
+
afirma("· installing the same skill twice is refused, never overwritten",
|
|
202
|
+
"already exists" in otra_vez, otra_vez)
|
|
203
|
+
sin = os.path.join(base, "not-a-skill")
|
|
204
|
+
os.makedirs(sin)
|
|
205
|
+
_, mal = ws.instala_skill(base, "urgencias", sin)
|
|
206
|
+
afirma("· a folder with no SKILL.md is not a skill", "SKILL.md" in mal, mal)
|
|
207
|
+
_, mal = ws.instala_skill(base, "urgencias", os.path.join(base, "ghost"))
|
|
208
|
+
afirma("· a source that is not there is an answer, not a traceback",
|
|
209
|
+
"nothing at" in mal, mal)
|
|
210
|
+
|
|
211
|
+
# A committed `.claude/skills` symlink would carry the install straight
|
|
212
|
+
# out of the agent's home — the global skills folder included.
|
|
213
|
+
fuera = os.path.join(base, "elsewhere")
|
|
214
|
+
os.makedirs(fuera)
|
|
215
|
+
hogar = ws.crea_workspace(base, "fugado")
|
|
216
|
+
os.makedirs(os.path.join(hogar, ".claude"), exist_ok=True)
|
|
217
|
+
os.symlink(fuera, os.path.join(hogar, ".claude", "skills"))
|
|
218
|
+
_, mal = ws.instala_skill(base, "fugado", origen)
|
|
219
|
+
afirma("· a symlinked skills root refuses the install and writes nothing",
|
|
220
|
+
"link out of its home" in mal and not os.listdir(fuera),
|
|
221
|
+
f"{mal} {os.listdir(fuera)}")
|
|
222
|
+
|
|
223
|
+
# A zip is the same skill by another door, through the same guards.
|
|
224
|
+
import io
|
|
225
|
+
import zipfile
|
|
226
|
+
crudo = os.path.join(base, "bomba.zip")
|
|
227
|
+
with zipfile.ZipFile(crudo, "w", zipfile.ZIP_DEFLATED) as z:
|
|
228
|
+
z.writestr("boom/SKILL.md", "---\nname: boom\n---\n")
|
|
229
|
+
z.writestr("boom/ceros.bin", b"\0" * (80 * 1024 * 1024))
|
|
230
|
+
_, mal = ws.instala_skill(base, "urgencias", crudo)
|
|
231
|
+
afirma("· a zip bomb is refused by what it inflates to, not what it weighs",
|
|
232
|
+
"inflates too large" in mal, mal)
|
|
233
|
+
bueno = os.path.join(base, "buena.zip")
|
|
234
|
+
with zipfile.ZipFile(bueno, "w") as z:
|
|
235
|
+
z.writestr("deploy/SKILL.md", "---\nname: deploy\n---\n")
|
|
236
|
+
nombre, mal = ws.instala_skill(base, "urgencias", bueno)
|
|
237
|
+
afirma("· a well-formed zip installs under its own folder name",
|
|
238
|
+
nombre == "deploy" and not mal, f"{nombre} {mal}")
|
|
239
|
+
del io
|
|
240
|
+
finally:
|
|
241
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
242
|
+
|
|
243
|
+
|
|
182
244
|
legacy_subsume()
|
|
183
245
|
nuevo_modelo()
|
|
246
|
+
skills_instaladas()
|
|
184
247
|
montajes()
|
|
185
248
|
colisiones_de_basename()
|
|
186
249
|
slugs_colisionados_rechazados()
|
package/bin/testlib.py
CHANGED
|
@@ -93,3 +93,26 @@ def resumen(etiqueta):
|
|
|
93
93
|
return 1
|
|
94
94
|
print(f' {etiqueta} ok — {HECHAS} checks\n')
|
|
95
95
|
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def roster(*agentes):
|
|
99
|
+
"""The roster shape the card writers take, from `(name, kind, role)` tuples.
|
|
100
|
+
|
|
101
|
+
Written once here because two suites build the same fixture, and a fixture
|
|
102
|
+
copied per file is how the tests stop agreeing with each other.
|
|
103
|
+
"""
|
|
104
|
+
import card
|
|
105
|
+
|
|
106
|
+
fuera = []
|
|
107
|
+
for entrada in agentes:
|
|
108
|
+
nombre, clase, rol = (list(entrada) + ['code', 'blank'])[:3]
|
|
109
|
+
fuera.append({
|
|
110
|
+
'nombre': nombre,
|
|
111
|
+
'slug': card.ventana(nombre),
|
|
112
|
+
'clase': clase,
|
|
113
|
+
'rol': rol,
|
|
114
|
+
'mounts': [],
|
|
115
|
+
'motor': {},
|
|
116
|
+
'skills': [],
|
|
117
|
+
})
|
|
118
|
+
return fuera
|
package/bin/update
ADDED