agents-city 0.4.0 → 0.5.2
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 +240 -11
- package/README.md +238 -11
- package/benchmarks/latency/fake-claude-cli.mjs +2 -1
- package/benchmarks/reception/README.md +16 -0
- package/benchmarks/reception/run.mjs +84 -0
- package/bin/agents-city.js +1 -0
- package/bin/connect +5 -0
- package/bin/hall.html +117 -18
- package/bin/navegador.mjs +170 -1
- package/bin/serve.py +201 -101
- package/bin/test +15 -3
- package/bin/test-arnes.py +192 -0
- package/bin/test-cage.py +9 -0
- package/bin/test-channel.py +178 -12
- package/bin/test-claude-runtime.py +132 -4
- package/bin/test-connect-client.mjs +609 -0
- package/bin/test-connect.py +52 -0
- package/bin/test-contracts.py +214 -0
- package/bin/test-diario.py +185 -0
- package/bin/test-doctor.py +31 -0
- package/bin/test-i18n.py +189 -0
- package/bin/test-navegador.py +61 -0
- package/bin/test-seat.py +78 -20
- package/bin/test-security.py +2 -0
- package/bin/test-serve.py +366 -0
- package/city/web/dist-hall/hall.js +789 -214
- package/city/web/src/bienvenida.ts +14 -13
- package/city/web/src/casa.ts +143 -56
- package/city/web/src/demo.ts +61 -31
- package/city/web/src/es.ts +187 -27
- package/city/web/src/explorador.ts +70 -43
- package/city/web/src/hall.ts +520 -103
- package/city/web/src/idioma.ts +21 -1
- package/city/web/src/motores.ts +48 -1
- package/city/web/src/vista.ts +58 -0
- package/demo/graba.py +5 -10
- package/docs/managed-connect.md +237 -0
- package/docs/security.md +41 -2
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +10 -0
- package/plugin/channel/adapter.js +78 -31
- package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
- package/plugin/channel/bus.js +109 -65
- package/plugin/channel/bus.ts +11 -1
- package/plugin/channel/client.js +67 -30
- package/plugin/channel/delivery-queue.ts +160 -21
- package/plugin/channel/hub/remote-roads.ts +32 -1
- package/plugin/channel/hub/road-controller.ts +116 -16
- package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
- package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
- package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
- package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
- package/plugin/channel/licenses/keyring-MIT.txt +21 -0
- package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
- package/plugin/channel/local-hub.js +2080 -97
- package/plugin/channel/local-hub.ts +5 -4
- package/plugin/channel/managed-connect/bridge.ts +208 -0
- package/plugin/channel/managed-connect/cli.ts +416 -0
- package/plugin/channel/managed-connect/device.ts +15 -0
- package/plugin/channel/managed-connect/local-cities.ts +94 -0
- package/plugin/channel/managed-connect/person-message.ts +74 -0
- package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
- package/plugin/channel/managed-connect/relay-session.ts +7 -0
- package/plugin/channel/managed-connect/storage.ts +653 -0
- package/plugin/channel/managed-connect/transport.ts +87 -0
- package/plugin/channel/managed-connect-cli.js +4777 -0
- package/plugin/channel/managed-connect-cli.ts +7 -0
- package/plugin/channel/managed-connect-client.d.ts +261 -0
- package/plugin/channel/managed-connect-client.js +6572 -0
- package/plugin/channel/managed-connect-client.manifest.json +38 -0
- package/plugin/channel/package-lock.json +123 -105
- package/plugin/channel/package.json +4 -4
- package/plugin/channel/protocol.ts +4 -0
- package/plugin/channel/reception.ts +896 -0
- package/plugin/channel/road-cli.ts +1 -1
- package/plugin/channel/runtime/arnes.json +189 -0
- package/plugin/channel/runtime/arnes.ts +47 -0
- package/plugin/channel/runtime/claude.ts +18 -0
- package/plugin/channel/runtime/codex-config.ts +51 -1
- package/plugin/channel/runtime/codex.ts +31 -11
- package/plugin/channel/runtime/kimi.ts +6 -4
- package/plugin/channel/runtime-files.ts +39 -5
- package/plugin/channel/runtime-gateway.js +426 -100
- package/plugin/channel/runtime-gateway.ts +12 -0
- package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
- package/plugin/scripts/arnes.py +271 -0
- package/plugin/scripts/busca.py +46 -10
- package/plugin/scripts/cage.py +5 -0
- package/plugin/scripts/city-session.sh +144 -28
- package/plugin/scripts/crecimiento.py +4 -1
- package/plugin/scripts/demos.py +58 -43
- package/plugin/scripts/desinstala.py +36 -21
- package/plugin/scripts/diario.py +119 -0
- package/plugin/scripts/doctor.py +90 -15
- package/plugin/scripts/read-card.py +48 -8
- package/plugin/scripts/reception.py +665 -0
package/bin/serve.py
CHANGED
|
@@ -23,10 +23,13 @@ import re
|
|
|
23
23
|
import secrets
|
|
24
24
|
import shutil
|
|
25
25
|
import signal
|
|
26
|
+
import sqlite3
|
|
26
27
|
import socketserver
|
|
28
|
+
import stat
|
|
27
29
|
import subprocess
|
|
28
30
|
import sys
|
|
29
31
|
import threading
|
|
32
|
+
import time
|
|
30
33
|
import urllib.parse
|
|
31
34
|
import webbrowser
|
|
32
35
|
|
|
@@ -40,9 +43,12 @@ import domains # noqa: E402
|
|
|
40
43
|
import gh # noqa: E402
|
|
41
44
|
import parcels # noqa: E402
|
|
42
45
|
import units # noqa: E402
|
|
46
|
+
import busca # noqa: E402 the disk scanner, and the one list of where work lives
|
|
43
47
|
import cities # noqa: E402
|
|
48
|
+
import diario # noqa: E402 what happened, written down
|
|
44
49
|
import demos # noqa: E402 the recorded demos the Hall plays back
|
|
45
50
|
import roads # noqa: E402
|
|
51
|
+
import reception # noqa: E402
|
|
46
52
|
import capabilities # noqa: E402
|
|
47
53
|
import deliberations # noqa: E402
|
|
48
54
|
import avatar # noqa: E402
|
|
@@ -187,6 +193,22 @@ def estado_seguro_agentes(datos):
|
|
|
187
193
|
return []
|
|
188
194
|
|
|
189
195
|
|
|
196
|
+
def resumen_recepcion_segura():
|
|
197
|
+
"""A damaged optional inbox must not make the whole local Hall disappear."""
|
|
198
|
+
try:
|
|
199
|
+
return reception.resumen()
|
|
200
|
+
except (OSError, sqlite3.Error, reception.ReceptionError) as e:
|
|
201
|
+
return {
|
|
202
|
+
"pending": 0,
|
|
203
|
+
"pendingBytes": 0,
|
|
204
|
+
"routingMode": "manual",
|
|
205
|
+
"reviewPolicy": "every_message",
|
|
206
|
+
"routerProfile": None,
|
|
207
|
+
"autoAvailable": False,
|
|
208
|
+
"error": str(e),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
190
212
|
def olvida_skills():
|
|
191
213
|
"""Forget the discovery memo: a skill was installed or removed, and that
|
|
192
214
|
changes the answer without touching the card."""
|
|
@@ -401,24 +423,6 @@ def historia_del_demo(datos):
|
|
|
401
423
|
historia = lee_clave(os.path.join(datos, "city.yml"), "domain")
|
|
402
424
|
return historia if historia in DEMO_STORIES else "software"
|
|
403
425
|
|
|
404
|
-
# Where people actually keep their code. The curses wizard offered `~/code` and
|
|
405
|
-
# nothing else, which is wrong on most machines and silently scans a folder that
|
|
406
|
-
# is not there. Offer the ones that exist, most-repos first, and let them type.
|
|
407
|
-
CANDIDATAS = [
|
|
408
|
-
"code",
|
|
409
|
-
"codigo",
|
|
410
|
-
"src",
|
|
411
|
-
"dev",
|
|
412
|
-
"devel",
|
|
413
|
-
"projects",
|
|
414
|
-
"proyectos",
|
|
415
|
-
"work",
|
|
416
|
-
"trabajo",
|
|
417
|
-
"repos",
|
|
418
|
-
"git",
|
|
419
|
-
"Developer",
|
|
420
|
-
"Documents",
|
|
421
|
-
]
|
|
422
426
|
|
|
423
427
|
|
|
424
428
|
def glob_fichas(datos):
|
|
@@ -521,33 +525,28 @@ def actividad_viva(datos):
|
|
|
521
525
|
return {"online": False, "url": "", "city": "", "started_at": ""}
|
|
522
526
|
|
|
523
527
|
|
|
528
|
+
def _resumen_cuerpo(cuerpo):
|
|
529
|
+
"""On success, the shape rather than the content: which fields arrived and
|
|
530
|
+
how long each was. A log that repeats every goal somebody types is a log
|
|
531
|
+
they will not send anywhere."""
|
|
532
|
+
if not isinstance(cuerpo, dict):
|
|
533
|
+
return {}
|
|
534
|
+
return {k: (len(v) if isinstance(v, (str, list)) else v) for k, v in cuerpo.items()}
|
|
535
|
+
|
|
536
|
+
|
|
524
537
|
def _que_es_git(ruta):
|
|
525
538
|
"""`repo`, `worktree`, or nothing. A label on a row, never a filter: the
|
|
526
539
|
picker shows every folder and lets the person decide which one matters."""
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
540
|
+
try:
|
|
541
|
+
# One stat, not an isdir followed by an isfile: a clone's `.git` is a
|
|
542
|
+
# directory and a linked worktree's is a file, and that is the whole
|
|
543
|
+
# question.
|
|
544
|
+
modo = os.stat(os.path.join(ruta, ".git")).st_mode
|
|
545
|
+
except OSError:
|
|
546
|
+
return ""
|
|
547
|
+
return "repo" if stat.S_ISDIR(modo) else "worktree"
|
|
533
548
|
|
|
534
549
|
|
|
535
|
-
def carpetas_probables():
|
|
536
|
-
"""Existing folders under $HOME that look like where the work lives."""
|
|
537
|
-
casa = os.path.expanduser("~")
|
|
538
|
-
fuera = []
|
|
539
|
-
for n in CANDIDATAS:
|
|
540
|
-
ruta = os.path.join(casa, n)
|
|
541
|
-
if not os.path.isdir(ruta):
|
|
542
|
-
continue
|
|
543
|
-
try:
|
|
544
|
-
hijos = [x for x in os.listdir(ruta) if not x.startswith(".")]
|
|
545
|
-
gits = sum(1 for x in hijos if os.path.isdir(os.path.join(ruta, x, ".git")))
|
|
546
|
-
except OSError:
|
|
547
|
-
continue
|
|
548
|
-
fuera.append({"ruta": ruta, "nombre": n, "items": len(hijos), "gits": gits})
|
|
549
|
-
fuera.sort(key=lambda c: (-c["gits"], -c["items"]))
|
|
550
|
-
return fuera
|
|
551
550
|
|
|
552
551
|
|
|
553
552
|
def catalogo_roles(dominio):
|
|
@@ -615,7 +614,27 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
615
614
|
if os.environ.get("CITY_SETUP_DEBUG"):
|
|
616
615
|
sys.stderr.write(" %s\n" % (format % args))
|
|
617
616
|
|
|
617
|
+
#: What this handler last answered, so the request log can say so without
|
|
618
|
+
#: every endpoint having to report for itself.
|
|
619
|
+
_ultimo = None
|
|
620
|
+
_error = None
|
|
621
|
+
|
|
622
|
+
def apunta(self, q, tipo, **campos):
|
|
623
|
+
"""One journal line, in the journal of the city the request acted on.
|
|
624
|
+
|
|
625
|
+
Not the selected one: a request carries `?city=`, and writing its line
|
|
626
|
+
into whichever city happened to be current put the record of what
|
|
627
|
+
happened to one city in another city's file. Which is worse than no
|
|
628
|
+
record, because it is a record that lies about where.
|
|
629
|
+
"""
|
|
630
|
+
try:
|
|
631
|
+
diario.apunta(self.ciudad(q), tipo, **campos)
|
|
632
|
+
except Exception: # noqa: BLE001 logging must not break the request
|
|
633
|
+
pass
|
|
634
|
+
|
|
618
635
|
def responde(self, cuerpo, tipo="application/json", codigo=200):
|
|
636
|
+
self._ultimo = codigo
|
|
637
|
+
self._error = cuerpo.get("error") if isinstance(cuerpo, dict) else None
|
|
619
638
|
if not isinstance(cuerpo, bytes):
|
|
620
639
|
cuerpo = json.dumps(cuerpo).encode()
|
|
621
640
|
self.send_response(codigo)
|
|
@@ -702,9 +721,11 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
702
721
|
"/api/instrucciones": "g_instrucciones",
|
|
703
722
|
"/api/live": "g_live",
|
|
704
723
|
"/api/carpeta": "g_carpeta",
|
|
724
|
+
"/api/diario": "g_diario",
|
|
705
725
|
"/api/demos": "g_demos",
|
|
706
726
|
"/api/domains": "g_domains",
|
|
707
727
|
"/api/roles": "g_roles",
|
|
728
|
+
"/api/reception": "g_reception",
|
|
708
729
|
}
|
|
709
730
|
POSTS = {
|
|
710
731
|
"/api/ficha": "p_ficha",
|
|
@@ -716,7 +737,9 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
716
737
|
"/api/ciudad-archiva": "p_archiva_ciudad",
|
|
717
738
|
"/api/ciudad-reinicia": "p_reinicia_ciudad",
|
|
718
739
|
"/api/roads": "p_roads",
|
|
740
|
+
"/api/reception": "p_reception",
|
|
719
741
|
"/api/agente": "p_agente",
|
|
742
|
+
"/api/diario": "p_diario",
|
|
720
743
|
"/api/agentes": "p_agentes",
|
|
721
744
|
"/api/montaje": "p_montaje",
|
|
722
745
|
"/api/motor": "p_motor",
|
|
@@ -769,9 +792,24 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
769
792
|
cuerpo = json.loads(self.rfile.read(largo) or b"{}")
|
|
770
793
|
except (ValueError, json.JSONDecodeError) as e:
|
|
771
794
|
return self.responde({"error": f"unreadable body: {e}"}, codigo=400)
|
|
772
|
-
if ruta in self.POSTS:
|
|
773
|
-
|
|
774
|
-
|
|
795
|
+
if ruta not in self.POSTS:
|
|
796
|
+
self.apunta(q, "post", ruta=ruta, estado=404, error="no such thing")
|
|
797
|
+
return self.responde({"error": "no such thing"}, codigo=404)
|
|
798
|
+
# Every write is recorded before it happens and judged after, so a
|
|
799
|
+
# request that never came back says so too — a handler that hangs or
|
|
800
|
+
# dies leaves a line with no verdict, which is itself the finding.
|
|
801
|
+
empezado = time.monotonic()
|
|
802
|
+
self._ultimo = None
|
|
803
|
+
try:
|
|
804
|
+
salida = getattr(self, self.POSTS[ruta])(q, cuerpo)
|
|
805
|
+
except Exception as e: # noqa: BLE001 the log is the point
|
|
806
|
+
self.apunta(q, "post", ruta=ruta, error=f"{type(e).__name__}: {e}",
|
|
807
|
+
cuerpo=cuerpo, ms=int((time.monotonic() - empezado) * 1000))
|
|
808
|
+
raise
|
|
809
|
+
self.apunta(q, "post", ruta=ruta, estado=self._ultimo,
|
|
810
|
+
cuerpo=cuerpo if self._ultimo != 200 else _resumen_cuerpo(cuerpo),
|
|
811
|
+
error=self._error, ms=int((time.monotonic() - empezado) * 1000))
|
|
812
|
+
return salida
|
|
775
813
|
|
|
776
814
|
def g_estado(self, q):
|
|
777
815
|
datos = self.ciudad(q)
|
|
@@ -837,6 +875,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
837
875
|
# Registration is earned by writing to one, never by looking.
|
|
838
876
|
"ciudades": lista_con(datos),
|
|
839
877
|
"roads": roads.lee(datos),
|
|
878
|
+
"reception": resumen_recepcion_segura(),
|
|
840
879
|
"invitation": roads.invitacion(datos, owner),
|
|
841
880
|
"skills": habilidades,
|
|
842
881
|
"deliberations": deliberations.lista(datos),
|
|
@@ -855,6 +894,13 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
855
894
|
def g_live(self, q):
|
|
856
895
|
return self.responde(actividad_viva(self.ciudad(q)))
|
|
857
896
|
|
|
897
|
+
def g_reception(self, q):
|
|
898
|
+
"""Owner-level remote quarantine; it is deliberately not city-scoped."""
|
|
899
|
+
try:
|
|
900
|
+
return self.responde(reception.estado(seat.quien_soy(), actual=self.ciudad(q)))
|
|
901
|
+
except (OSError, sqlite3.Error, reception.ReceptionError) as e:
|
|
902
|
+
return self.responde({"error": str(e)}, codigo=503)
|
|
903
|
+
|
|
858
904
|
def g_demos(self, q):
|
|
859
905
|
"""The recorded demos, and one of them in full when asked by name.
|
|
860
906
|
|
|
@@ -864,18 +910,24 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
864
910
|
the browser owning a second, drifting copy of the state machine.
|
|
865
911
|
"""
|
|
866
912
|
cual = q.get("story", [""])[0]
|
|
867
|
-
catalogo = demos.catalogo()
|
|
868
913
|
if not cual:
|
|
869
|
-
return self.responde({"demos": catalogo})
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
return self.responde({"error": "no such demo"}, codigo=404)
|
|
914
|
+
return self.responde({"demos": demos.catalogo()})
|
|
915
|
+
# One read of one file: the card is derived from the very events being
|
|
916
|
+
# served, rather than building the whole shelf to look one row up.
|
|
873
917
|
eventos = demos.eventos(cual)
|
|
874
918
|
if not eventos:
|
|
875
|
-
return self.responde(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
919
|
+
return self.responde({"error": "no such demo"}, codigo=404)
|
|
920
|
+
return self.responde({**(demos.ficha(cual, eventos) or {}), "eventos": eventos})
|
|
921
|
+
|
|
922
|
+
def g_diario(self, q):
|
|
923
|
+
"""The journal, for `doctor --log` and for anybody about to send it."""
|
|
924
|
+
try:
|
|
925
|
+
cuantas = max(1, min(2000, int(q.get("n", ["200"])[0])))
|
|
926
|
+
except ValueError:
|
|
927
|
+
cuantas = 200
|
|
928
|
+
return self.responde(
|
|
929
|
+
{"ruta": diario.ruta(self.ciudad(q)), "lineas": diario.lee(self.ciudad(q), cuantas)}
|
|
930
|
+
)
|
|
879
931
|
|
|
880
932
|
def g_carpeta(self, q):
|
|
881
933
|
"""One folder, listed: what is in it, and what each thing is.
|
|
@@ -911,10 +963,6 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
911
963
|
"nombre": e.name,
|
|
912
964
|
"ruta": os.path.join(ruta, e.name),
|
|
913
965
|
"dir": es_dir,
|
|
914
|
-
# What kind of place this is, said plainly rather
|
|
915
|
-
# than used to filter anything out: a folder is
|
|
916
|
-
# yours to pick whether or not it holds a .git.
|
|
917
|
-
"git": _que_es_git(os.path.join(ruta, e.name)) if es_dir else "",
|
|
918
966
|
"enlace": e.is_symlink(),
|
|
919
967
|
}
|
|
920
968
|
)
|
|
@@ -923,40 +971,31 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
923
971
|
# Folders first, then files, each alphabetical and case-insensitive —
|
|
924
972
|
# the order every file picker has used for forty years.
|
|
925
973
|
entradas.sort(key=lambda x: (not x["dir"], x["nombre"].lower()))
|
|
974
|
+
recortada = len(entradas) > 2000
|
|
975
|
+
entradas = entradas[:2000]
|
|
976
|
+
# Only now, and only for the rows that will be sent. Asking every entry
|
|
977
|
+
# in a twenty-thousand-item directory whether it holds a `.git` was
|
|
978
|
+
# eighteen thousand pairs of stat calls for rows nobody would see. What
|
|
979
|
+
# kind of place this is gets said plainly rather than used to filter
|
|
980
|
+
# anything out: a folder is yours to pick whether or not it holds one.
|
|
981
|
+
for e in entradas:
|
|
982
|
+
e["git"] = _que_es_git(e["ruta"]) if e["dir"] else ""
|
|
926
983
|
casa = os.path.expanduser("~")
|
|
927
984
|
arriba = os.path.dirname(ruta)
|
|
928
985
|
return self.responde(
|
|
929
986
|
{
|
|
930
987
|
"ruta": ruta,
|
|
931
988
|
"arriba": arriba if arriba != ruta else "",
|
|
932
|
-
"casa": casa,
|
|
933
989
|
"atajos": [
|
|
934
990
|
{"nombre": os.path.basename(c) or c, "ruta": c}
|
|
935
|
-
for c in [casa] + [os.path.join(casa, n) for n in CANDIDATAS]
|
|
991
|
+
for c in [casa] + [os.path.join(casa, n) for n in busca.CANDIDATAS]
|
|
936
992
|
if os.path.isdir(c)
|
|
937
993
|
],
|
|
938
|
-
"entradas": entradas
|
|
939
|
-
"recortada":
|
|
994
|
+
"entradas": entradas,
|
|
995
|
+
"recortada": recortada,
|
|
940
996
|
}
|
|
941
997
|
)
|
|
942
998
|
|
|
943
|
-
def g_arranque(self, q):
|
|
944
|
-
plants = W.plantillas()
|
|
945
|
-
return self.responde(
|
|
946
|
-
{
|
|
947
|
-
"kinds": plants,
|
|
948
|
-
"destino": DESTINO,
|
|
949
|
-
"casa": os.path.expanduser("~"),
|
|
950
|
-
"carpetas": carpetas_probables(),
|
|
951
|
-
# Only whether the CLI is there. Whether it is logged in is the
|
|
952
|
-
# orgs endpoint's answer, and asking it here would make every
|
|
953
|
-
# page load wait on a network call.
|
|
954
|
-
"gh": bool(shutil.which("gh")),
|
|
955
|
-
"existe": os.path.isdir(DESTINO),
|
|
956
|
-
"paleta": [{"hex": c, "nombre": n} for c, n in W.PALETA],
|
|
957
|
-
"arquitectos": sorted(ARQUITECTOS),
|
|
958
|
-
}
|
|
959
|
-
)
|
|
960
999
|
|
|
961
1000
|
def g_roles(self, q):
|
|
962
1001
|
if q.get("scope", [""])[0] == "agent":
|
|
@@ -974,29 +1013,7 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
974
1013
|
}
|
|
975
1014
|
)
|
|
976
1015
|
|
|
977
|
-
def g_orgs(self, q):
|
|
978
|
-
return self.responde({"orgs": W.orgs_de_gh()})
|
|
979
1016
|
|
|
980
|
-
def g_repos(self, q):
|
|
981
|
-
org = q.get("org", [""])[0]
|
|
982
|
-
if org:
|
|
983
|
-
hallados = [
|
|
984
|
-
{"nombre": n, "detalle": d, "fecha": f, "ruta": ""}
|
|
985
|
-
for n, d, f in W.repos_de_org(org)
|
|
986
|
-
]
|
|
987
|
-
return self.responde({"repos": hallados})
|
|
988
|
-
base = os.path.expanduser(q.get("ruta", [""])[0])
|
|
989
|
-
if not base or not os.path.isdir(base):
|
|
990
|
-
return self.responde({"repos": [], "error": f"{base or '(empty)'} is not a folder"})
|
|
991
|
-
# Two different third columns, and saying so beats guessing in the page:
|
|
992
|
-
# a git repo reports its last commit, any-folder reports what is inside.
|
|
993
|
-
if q.get("modo", ["git"])[0] == "git":
|
|
994
|
-
crudo = W.repos_del_disco(base)
|
|
995
|
-
repos = [{"nombre": n, "ruta": r, "detalle": "", "fecha": f} for n, r, f in crudo]
|
|
996
|
-
else:
|
|
997
|
-
crudo = W.carpetas_del_disco(base)
|
|
998
|
-
repos = [{"nombre": n, "ruta": r, "detalle": d, "fecha": ""} for n, r, d in crudo]
|
|
999
|
-
return self.responde({"repos": repos})
|
|
1000
1017
|
|
|
1001
1018
|
def g_gente(self, q):
|
|
1002
1019
|
org = q.get("org", [""])[0]
|
|
@@ -1242,6 +1259,51 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1242
1259
|
return self.responde({"error": str(e)}, codigo=400)
|
|
1243
1260
|
return self.responde({"ok": True, "roads": roads.lee(origen)})
|
|
1244
1261
|
|
|
1262
|
+
def p_reception(self, q, cuerpo):
|
|
1263
|
+
"""One human decision, committed before any city can consume the text."""
|
|
1264
|
+
if cuerpo.get("action") == "configure":
|
|
1265
|
+
try:
|
|
1266
|
+
return self.responde(
|
|
1267
|
+
reception.configura(
|
|
1268
|
+
seat.quien_soy(),
|
|
1269
|
+
cuerpo.get("routing_mode"),
|
|
1270
|
+
cuerpo.get("rules"),
|
|
1271
|
+
self.ciudad(q),
|
|
1272
|
+
)
|
|
1273
|
+
)
|
|
1274
|
+
except reception.ReceptionError as e:
|
|
1275
|
+
return self.responde({"error": str(e)}, codigo=400)
|
|
1276
|
+
except (OSError, sqlite3.Error) as e:
|
|
1277
|
+
return self.responde({"error": f"reception unavailable: {e}"}, codigo=503)
|
|
1278
|
+
if cuerpo.get("action") == "send":
|
|
1279
|
+
try:
|
|
1280
|
+
return self.responde(
|
|
1281
|
+
reception.envia(cuerpo.get("connection_id"), cuerpo.get("text")),
|
|
1282
|
+
codigo=202,
|
|
1283
|
+
)
|
|
1284
|
+
except reception.ReceptionError as e:
|
|
1285
|
+
return self.responde({"error": str(e)}, codigo=400)
|
|
1286
|
+
except (OSError, sqlite3.Error) as e:
|
|
1287
|
+
return self.responde({"error": f"reception unavailable: {e}"}, codigo=503)
|
|
1288
|
+
destinos = cuerpo.get("destinations") or []
|
|
1289
|
+
if not isinstance(destinos, list):
|
|
1290
|
+
return self.responde({"error": "destinations must be a list"}, codigo=400)
|
|
1291
|
+
try:
|
|
1292
|
+
resultado = reception.decide(
|
|
1293
|
+
seat.quien_soy(),
|
|
1294
|
+
cuerpo.get("message_id"),
|
|
1295
|
+
cuerpo.get("action"),
|
|
1296
|
+
destinos,
|
|
1297
|
+
cuerpo.get("reason"),
|
|
1298
|
+
self.ciudad(q),
|
|
1299
|
+
)
|
|
1300
|
+
except reception.ReceptionError as e:
|
|
1301
|
+
codigo = 409 if "already" in str(e) or "conflict" in str(e) else 400
|
|
1302
|
+
return self.responde({"error": str(e)}, codigo=codigo)
|
|
1303
|
+
except (OSError, sqlite3.Error) as e:
|
|
1304
|
+
return self.responde({"error": f"reception unavailable: {e}"}, codigo=503)
|
|
1305
|
+
return self.responde(resultado)
|
|
1306
|
+
|
|
1245
1307
|
def p_mapa(self, q, cuerpo):
|
|
1246
1308
|
# Bake and serve the map, detached. First run builds the front end and
|
|
1247
1309
|
# seeds the local D1, so the page polls /api/estado until it is up.
|
|
@@ -1290,7 +1352,12 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1290
1352
|
# nothing, never a shell fragment". `runtime` maps to `runs.` — the launcher's
|
|
1291
1353
|
# own key — and the web deliberately cannot reach the terminal: fallback.
|
|
1292
1354
|
CAMPOS_DE_AGENTE = {
|
|
1293
|
-
|
|
1355
|
+
# A model name is whatever the CLI in play calls it: `opus`, but also
|
|
1356
|
+
# `gpt-5.6-sol` and OpenCode's `anthropic/claude-sonnet-4`. Slash,
|
|
1357
|
+
# underscore and colon are in; whitespace, quotes and every shell
|
|
1358
|
+
# metacharacter stay out, because this value ends up on a command line.
|
|
1359
|
+
"model": ("model", re.compile(r"[a-z0-9][a-z0-9./:_-]{0,63}"),
|
|
1360
|
+
"that is not a model name"),
|
|
1294
1361
|
"effort": ("effort", frozenset(("low", "medium", "high", "xhigh", "max")),
|
|
1295
1362
|
"effort is low..max or empty"),
|
|
1296
1363
|
"runtime": ("runs", frozenset(("claude",) + seat.OTROS_MOTORES),
|
|
@@ -1314,8 +1381,23 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1314
1381
|
return self.responde({"error": "this city has no owner card yet"}, codigo=409)
|
|
1315
1382
|
nombre = " ".join(str(cuerpo.get("name") or "").split())
|
|
1316
1383
|
slug = card.ventana(nombre)
|
|
1317
|
-
|
|
1384
|
+
# `card.ventana` falls back to the word `repo` when nothing in the name
|
|
1385
|
+
# survives slugging — which is right for a repository whose folder is
|
|
1386
|
+
# punctuation, and wrong here: a house called `///` would be created
|
|
1387
|
+
# with the window `repo`, and the name a person reads would stop being
|
|
1388
|
+
# the thing the city addresses. A name has to carry a letter or a digit.
|
|
1389
|
+
legible = bool(re.search(r"[a-z0-9]", nombre.lower()))
|
|
1390
|
+
if not nombre or not card.ventana_valida(slug) or not legible:
|
|
1318
1391
|
return self.responde({"error": "an agent needs a plain name"}, codigo=400)
|
|
1392
|
+
# A window slug is cut at 80 characters, so a longer name would be
|
|
1393
|
+
# stored in full on the card and addressed by a truncated one — the
|
|
1394
|
+
# name a person reads and the window it opens quietly stop being the
|
|
1395
|
+
# same thing. Refuse rather than silently rename.
|
|
1396
|
+
if len(nombre) > 80:
|
|
1397
|
+
return self.responde(
|
|
1398
|
+
{"error": "an agent's name has to fit in a window title: 80 characters"},
|
|
1399
|
+
codigo=400,
|
|
1400
|
+
)
|
|
1319
1401
|
clase = str(cuerpo.get("kind") or workspace.CLASE_DEFECTO).strip().lower()
|
|
1320
1402
|
if clase not in workspace.CLASES:
|
|
1321
1403
|
return self.responde(
|
|
@@ -1410,6 +1492,24 @@ class Manejador(http.server.BaseHTTPRequestHandler):
|
|
|
1410
1492
|
}
|
|
1411
1493
|
)
|
|
1412
1494
|
|
|
1495
|
+
def p_diario(self, q, cuerpo):
|
|
1496
|
+
"""The browser's half of the log.
|
|
1497
|
+
|
|
1498
|
+
Half of what goes wrong here goes wrong in the page — a handler that
|
|
1499
|
+
threw, a fetch that never came back, a button that did nothing. A log
|
|
1500
|
+
that stops at the network boundary tells half the story, so the page
|
|
1501
|
+
writes into the same file the server does, and one file answers "what
|
|
1502
|
+
happened" instead of two that have to be lined up by hand.
|
|
1503
|
+
"""
|
|
1504
|
+
diario.apunta(
|
|
1505
|
+
self.ciudad(q),
|
|
1506
|
+
"browser",
|
|
1507
|
+
que=str(cuerpo.get("que") or "")[:120],
|
|
1508
|
+
detalle=cuerpo.get("detalle"),
|
|
1509
|
+
donde=str(cuerpo.get("donde") or "")[:200],
|
|
1510
|
+
)
|
|
1511
|
+
return self.responde({"ok": True})
|
|
1512
|
+
|
|
1413
1513
|
def p_agente(self, q, cuerpo):
|
|
1414
1514
|
"""Tune one agent from its character sheet: model, effort, runtime and
|
|
1415
1515
|
avatar seed, written to the card keys the launcher already resolves. An
|
package/bin/test
CHANGED
|
@@ -22,7 +22,7 @@ if [ -d .git ] && [ -x .githooks/pre-commit ] \
|
|
|
22
22
|
&& printf ' pre-commit gate armed (core.hooksPath .githooks)\n'
|
|
23
23
|
fi
|
|
24
24
|
|
|
25
|
-
SUITES=(widgets card parcels domains busca serve seat cities channel committee live-feed runtime claude-runtime runtime-ui runtime-failures stress adapter benchmark demo contracts exit rutas cage broker evidencia admision pairing hall-protocol doctor security workspace avatar crecimiento atajos navegador actualiza desinstala launch)
|
|
25
|
+
SUITES=(widgets card parcels domains busca arnes i18n diario serve seat cities channel connect committee live-feed runtime claude-runtime runtime-ui runtime-failures stress adapter benchmark demo contracts exit rutas cage broker evidencia admision pairing hall-protocol doctor security workspace avatar crecimiento atajos navegador actualiza desinstala launch)
|
|
26
26
|
[ $# -gt 0 ] && SUITES=("$@")
|
|
27
27
|
|
|
28
28
|
fallos=0
|
|
@@ -90,16 +90,27 @@ necesarios = ["bin/agents-city.js", "bin/hall", "bin/seat", "bin/city", "bin/dem
|
|
|
90
90
|
"plugin/scripts/workspace.py", "plugin/scripts/avatar.py",
|
|
91
91
|
"plugin/scripts/crecimiento.py",
|
|
92
92
|
"plugin/scripts/reset.py", "plugin/scripts/desinstala.py",
|
|
93
|
-
"plugin/scripts/demos.py", "plugin/scripts/
|
|
93
|
+
"plugin/scripts/demos.py", "plugin/scripts/arnes.py",
|
|
94
|
+
"plugin/scripts/diario.py",
|
|
95
|
+
"plugin/channel/runtime/arnes.json", "plugin/scripts/capabilities.py",
|
|
94
96
|
"plugin/scripts/deliberations.py",
|
|
95
97
|
"plugin/scripts/runtime_processes.py",
|
|
96
98
|
"plugin/scripts/runtime_log.py", "plugin/scripts/logs.py",
|
|
97
99
|
"plugin/scripts/launch.py", "plugin/scripts/hook_activity.py",
|
|
100
|
+
"plugin/scripts/reception.py",
|
|
98
101
|
"plugin/scripts/city-session.sh", "plugin/scripts/city-runtime.sh",
|
|
99
102
|
"plugin/scripts/find-repos.sh",
|
|
100
103
|
"plugin/scripts/busca.py",
|
|
101
104
|
"plugin/channel/bus.js", "plugin/channel/local-hub.js",
|
|
102
105
|
"plugin/channel/client.js", "plugin/channel/adapter.js",
|
|
106
|
+
"plugin/channel/managed-connect-client.js",
|
|
107
|
+
"plugin/channel/managed-connect-client.d.ts",
|
|
108
|
+
"plugin/channel/managed-connect-client.manifest.json",
|
|
109
|
+
"plugin/channel/trust/agents-city-sandbox-roots.json",
|
|
110
|
+
"plugin/channel/kinsh_vodozemac_wasm_bg.wasm",
|
|
111
|
+
"plugin/channel/agents_city_hybrid_crypto_bg.wasm",
|
|
112
|
+
"plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md",
|
|
113
|
+
"plugin/channel/managed-connect-cli.js", "bin/connect",
|
|
103
114
|
"plugin/channel/runtime-gateway.js",
|
|
104
115
|
"plugin/.mcp.json",
|
|
105
116
|
"bin/cities", "bin/road", "bin/bus", "bin/committee", "bin/agents",
|
|
@@ -111,7 +122,8 @@ necesarios = ["bin/agents-city.js", "bin/hall", "bin/seat", "bin/city", "bin/dem
|
|
|
111
122
|
"benchmarks/latency/fake-native-server.mjs",
|
|
112
123
|
"benchmarks/latency/fake-claude-cli.mjs",
|
|
113
124
|
"benchmarks/stress/run.py", "benchmarks/stress/README.md",
|
|
114
|
-
"
|
|
125
|
+
"benchmarks/reception/run.mjs", "benchmarks/reception/README.md",
|
|
126
|
+
"city/web/src/repo-roles.ts", "city/web/src/vista.ts",
|
|
115
127
|
"demo/show.py", "demo/city/city.yml", "demo/city/ada.md",
|
|
116
128
|
"city/scripts/seed.py", "city/worker/schema.sql", "demo/parcels.yml"]
|
|
117
129
|
problemas = [f"missing: {n}" for n in necesarios if n not in dentro]
|