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.
Files changed (98) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +240 -11
  3. package/README.md +238 -11
  4. package/benchmarks/latency/fake-claude-cli.mjs +2 -1
  5. package/benchmarks/reception/README.md +16 -0
  6. package/benchmarks/reception/run.mjs +84 -0
  7. package/bin/agents-city.js +1 -0
  8. package/bin/connect +5 -0
  9. package/bin/hall.html +117 -18
  10. package/bin/navegador.mjs +170 -1
  11. package/bin/serve.py +201 -101
  12. package/bin/test +15 -3
  13. package/bin/test-arnes.py +192 -0
  14. package/bin/test-cage.py +9 -0
  15. package/bin/test-channel.py +178 -12
  16. package/bin/test-claude-runtime.py +132 -4
  17. package/bin/test-connect-client.mjs +609 -0
  18. package/bin/test-connect.py +52 -0
  19. package/bin/test-contracts.py +214 -0
  20. package/bin/test-diario.py +185 -0
  21. package/bin/test-doctor.py +31 -0
  22. package/bin/test-i18n.py +189 -0
  23. package/bin/test-navegador.py +61 -0
  24. package/bin/test-seat.py +78 -20
  25. package/bin/test-security.py +2 -0
  26. package/bin/test-serve.py +366 -0
  27. package/city/web/dist-hall/hall.js +789 -214
  28. package/city/web/src/bienvenida.ts +14 -13
  29. package/city/web/src/casa.ts +143 -56
  30. package/city/web/src/demo.ts +61 -31
  31. package/city/web/src/es.ts +187 -27
  32. package/city/web/src/explorador.ts +70 -43
  33. package/city/web/src/hall.ts +520 -103
  34. package/city/web/src/idioma.ts +21 -1
  35. package/city/web/src/motores.ts +48 -1
  36. package/city/web/src/vista.ts +58 -0
  37. package/demo/graba.py +5 -10
  38. package/docs/managed-connect.md +237 -0
  39. package/docs/security.md +41 -2
  40. package/package.json +8 -4
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/channel/adapter-prompts.ts +10 -0
  43. package/plugin/channel/adapter.js +78 -31
  44. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  45. package/plugin/channel/bus.js +109 -65
  46. package/plugin/channel/bus.ts +11 -1
  47. package/plugin/channel/client.js +67 -30
  48. package/plugin/channel/delivery-queue.ts +160 -21
  49. package/plugin/channel/hub/remote-roads.ts +32 -1
  50. package/plugin/channel/hub/road-controller.ts +116 -16
  51. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  52. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  53. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  54. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  55. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  56. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  57. package/plugin/channel/local-hub.js +2080 -97
  58. package/plugin/channel/local-hub.ts +5 -4
  59. package/plugin/channel/managed-connect/bridge.ts +208 -0
  60. package/plugin/channel/managed-connect/cli.ts +416 -0
  61. package/plugin/channel/managed-connect/device.ts +15 -0
  62. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  63. package/plugin/channel/managed-connect/person-message.ts +74 -0
  64. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  65. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  66. package/plugin/channel/managed-connect/storage.ts +653 -0
  67. package/plugin/channel/managed-connect/transport.ts +87 -0
  68. package/plugin/channel/managed-connect-cli.js +4777 -0
  69. package/plugin/channel/managed-connect-cli.ts +7 -0
  70. package/plugin/channel/managed-connect-client.d.ts +261 -0
  71. package/plugin/channel/managed-connect-client.js +6572 -0
  72. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  73. package/plugin/channel/package-lock.json +123 -105
  74. package/plugin/channel/package.json +4 -4
  75. package/plugin/channel/protocol.ts +4 -0
  76. package/plugin/channel/reception.ts +896 -0
  77. package/plugin/channel/road-cli.ts +1 -1
  78. package/plugin/channel/runtime/arnes.json +189 -0
  79. package/plugin/channel/runtime/arnes.ts +47 -0
  80. package/plugin/channel/runtime/claude.ts +18 -0
  81. package/plugin/channel/runtime/codex-config.ts +51 -1
  82. package/plugin/channel/runtime/codex.ts +31 -11
  83. package/plugin/channel/runtime/kimi.ts +6 -4
  84. package/plugin/channel/runtime-files.ts +39 -5
  85. package/plugin/channel/runtime-gateway.js +426 -100
  86. package/plugin/channel/runtime-gateway.ts +12 -0
  87. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  88. package/plugin/scripts/arnes.py +271 -0
  89. package/plugin/scripts/busca.py +46 -10
  90. package/plugin/scripts/cage.py +5 -0
  91. package/plugin/scripts/city-session.sh +144 -28
  92. package/plugin/scripts/crecimiento.py +4 -1
  93. package/plugin/scripts/demos.py +58 -43
  94. package/plugin/scripts/desinstala.py +36 -21
  95. package/plugin/scripts/diario.py +119 -0
  96. package/plugin/scripts/doctor.py +90 -15
  97. package/plugin/scripts/read-card.py +48 -8
  98. package/plugin/scripts/reception.py +665 -0
@@ -22,6 +22,7 @@ import sys
22
22
 
23
23
  GUIONES = os.path.dirname(os.path.abspath(__file__))
24
24
  sys.path.insert(0, GUIONES)
25
+ import arnes # noqa: E402 what we do to the CLIs somebody already had
25
26
  from dataclasses import dataclass
26
27
  from typing import Callable
27
28
 
@@ -188,15 +189,23 @@ def revisa_entorno():
188
189
  fuera.append(('python', True, _sys.version.split()[0]))
189
190
  # Every one of these spells its version flag differently, and tmux answers
190
191
  # `--version` with an error that reads like a working version string.
191
- for programa, bandera, obligatorio in (('tmux', '-V', True), ('bash', '--version', True),
192
- ('git', '--version', True),
193
- ('node', '--version', True),
194
- ('gh', '--version', False)):
195
- v = _version_de(programa, bandera)
192
+ #
193
+ # Five independent subprocesses: asked together, the wait is the slowest one
194
+ # rather than the sum of five. A doctor that takes half a second to say
195
+ # everything is fine gets run; one that takes two does not.
196
+ programas = (('tmux', '-V', True), ('bash', '--version', True),
197
+ ('git', '--version', True), ('node', '--version', True),
198
+ ('gh', '--version', False))
199
+ from concurrent.futures import ThreadPoolExecutor # noqa: PLC0415
200
+
201
+ with ThreadPoolExecutor(max_workers=len(programas)) as piscina:
202
+ versiones = list(piscina.map(lambda p: _version_de(p[0], p[1]), programas))
203
+ for (programa, _bandera, obligatorio), v in zip(programas, versiones, strict=True):
196
204
  fuera.append((programa, bool(v) if obligatorio else (True if v else None),
197
205
  v or ('missing' if obligatorio else 'not installed (optional)')))
198
206
 
199
- motores = [m for m in ('claude', 'codex', 'opencode', 'kimi') if _sh.which(m)]
207
+ # The declaration owns which runtimes exist and what each is called on disk.
208
+ motores = [e['binario'] for e in arnes.informe() if e['instalado']]
200
209
  fuera.append(('runtimes', bool(motores),
201
210
  ', '.join(motores) if motores else 'none — a city needs at least one CLI'))
202
211
 
@@ -257,22 +266,88 @@ def informe_entorno():
257
266
  return 1 if roto else 0
258
267
 
259
268
 
260
- def main():
269
+ def _diario(empaqueta, resto):
270
+ """`doctor --log` reads the journal; `doctor --report` bundles it to send.
271
+
272
+ The report is the thing that was missing. When this breaks on somebody
273
+ else's machine, the useful answer is not "tell me what you did" — it is a
274
+ file they can attach without reading it first. So it carries the journal and
275
+ the environment report, and nothing else: `diario` redacts credentials on
276
+ the way in, so what lands here was never a secret to begin with.
277
+ """
278
+ import cities # noqa: PLC0415
279
+ import diario # noqa: PLC0415
280
+
281
+ datos = cities.actual()
282
+ try:
283
+ cuantas = int(resto[0]) if resto else (2000 if empaqueta else 60)
284
+ except ValueError:
285
+ cuantas = 60
286
+ lineas = diario.lee(datos, cuantas)
287
+ if not empaqueta:
288
+ print(f'\n {diario.ruta(datos)}\n')
289
+ if not lineas:
290
+ print(' Nothing recorded yet. The Hall writes here as you use it.\n')
291
+ return 0
292
+ for l in lineas:
293
+ resto_l = {k: v for k, v in l.items() if k not in ('t', 'tipo')}
294
+ detalle = json.dumps(resto_l, ensure_ascii=False)[:160]
295
+ print(f" {l.get('t', '')} {l.get('tipo', ''):8} {detalle}")
296
+ print()
297
+ return 0
298
+
299
+ import io # noqa: PLC0415
300
+ from contextlib import redirect_stdout # noqa: PLC0415
301
+
302
+ entorno = io.StringIO()
303
+ with redirect_stdout(entorno):
304
+ informe_entorno()
305
+ destino = os.path.join(os.path.expanduser('~'), 'agents-city-report.txt')
306
+ with open(destino, 'w', encoding='utf-8') as f:
307
+ f.write('# agents-city report\n\n')
308
+ f.write('## this machine\n')
309
+ f.write(entorno.getvalue())
310
+ f.write(f'\n## the journal ({len(lineas)} entries)\n\n')
311
+ for l in lineas:
312
+ f.write(json.dumps(l, ensure_ascii=False) + '\n')
313
+ print(f'\n Written to {destino}\n')
314
+ print(' It holds what this machine is and what the town hall did.')
315
+ print(' Credentials are stripped as they are recorded, so it is safe to attach.\n')
316
+ return 0
317
+
318
+
319
+ def main(argv=None):
320
+ # The arguments are a parameter so this door can be knocked on from a test
321
+ # without a subprocess: `doctor --config` is the command that backs a claim
322
+ # about somebody's machine, and it deserves to be checked like one.
323
+ argv = list(sys.argv if argv is None else argv)
261
324
  # `doctor` with no file is the environment report — what a person means by
262
325
  # the word. The config migration keeps its own path, unchanged.
263
- if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help', 'help'):
264
- if len(sys.argv) > 1:
265
- print(' usage: agents-city doctor [config.json [--fix]]\n\n'
266
- ' With no arguments: check this machine — tools, runtimes, the\n'
267
- ' cage, the selected city, and whether a newer version is out.\n'
268
- ' With a config file: detect, explain and migrate an old shape.')
269
- return 0
326
+ if len(argv) > 1 and argv[1] in ('-h', '--help', 'help'):
327
+ print(
328
+ ' usage: agents-city doctor [--config] [config.json [--fix]]\n\n'
329
+ ' With no arguments: check this machine — tools, runtimes, the\n'
330
+ ' cage, the selected city, and whether a newer version is out.\n'
331
+ ' --config: what this product does to the CLIs you already have —\n'
332
+ ' what it adds, what it inherits from your own settings, and what\n'
333
+ ' it leaves alone. Add --json for the same thing as data.\n'
334
+ ' With a config file: detect, explain and migrate an old shape.'
335
+ )
336
+ return 0
337
+ if len(argv) == 1:
270
338
  return informe_entorno()
339
+ # The claim this product makes about somebody else's machine, printed so it
340
+ # can be checked instead of believed.
341
+ if argv[1] in ('--config', 'config'):
342
+ return arnes.main(argv[2:])
343
+ # What happened, and something to attach to an issue.
344
+ if argv[1] in ('--log', 'log', '--report', 'report'):
345
+ return _diario(argv[1] in ('--report', 'report'), argv[2:])
271
346
  p = argparse.ArgumentParser(description='Detect, explain and migrate an old config shape.')
272
347
  p.add_argument('fichero', help='the config JSON file to check')
273
348
  p.add_argument('--fix', action='store_true', help='rewrite (default is dry-run report)')
274
349
  p.add_argument('--stamp', default='manual', help='backup suffix (tests pass a fixed value)')
275
- args = p.parse_args()
350
+ args = p.parse_args(argv[1:])
276
351
  try:
277
352
  reporte = cura_fichero(args.fichero, args.stamp, dry_run=not args.fix)
278
353
  except (OSError, json.JSONDecodeError) as e:
@@ -5,6 +5,9 @@
5
5
  read-card.py <card.md> repos -> comma-separated list
6
6
  read-card.py --window <repo> -> canonical tmux/bus actor slug
7
7
  read-card.py --actor-role <card.md> <actor> -> safe operating role or blank
8
+ read-card.py --varios <card.md> f1 f2 ... -> one line per field, in order
9
+ read-card.py --ventana <card.md> <window> -> everything the launcher needs
10
+ for that window, one per line
8
11
 
9
12
  A thin front on card.py, which is where the parsing lives. It used to hold its own
10
13
  copy, and that copy is why the tmux session asked for `agente` — a field no card in
@@ -19,6 +22,16 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
22
  import card # noqa: E402
20
23
 
21
24
 
25
+ def _campo(texto, campo):
26
+ valor = card.campo(texto, campo)
27
+ # A bracketed value is a list everywhere else in this product — `repos:`,
28
+ # `agents:`, `mounts.<agent>:` — and the shell wants one line. Deciding by
29
+ # SHAPE rather than by a list of blessed field names is what stopped this
30
+ # from having to be edited every time the card grew another list.
31
+ esLista = valor.strip().startswith('[') and valor.strip().endswith(']')
32
+ return ','.join(card.lista(valor)) if esLista else valor
33
+
34
+
22
35
  def main():
23
36
  if len(sys.argv) == 3 and sys.argv[1] == '--window':
24
37
  print(card.ventana(sys.argv[2]))
@@ -31,10 +44,43 @@ def main():
31
44
  actor = card.ventana(sys.argv[3])
32
45
  print(card.rol_seguro(card.campo(texto, f'role.{actor}')))
33
46
  return
47
+ # Everything one window needs, in one interpreter.
48
+ #
49
+ # The launcher used to ask three times per window — the operating role, what
50
+ # runs there, and the engine — each a fresh Python start re-reading the same
51
+ # small card. Six lines, in a fixed order, because the shell reads them with
52
+ # `sed -n Np` and a named format would mean parsing on that side too:
53
+ #
54
+ # 1 role 2 runs 3 model.<window> 4 model 5 effort.<window> 6 effort
55
+ if len(sys.argv) == 4 and sys.argv[1] == '--ventana':
56
+ try:
57
+ with open(sys.argv[2], encoding='utf-8') as f:
58
+ texto = f.read()
59
+ except OSError as e:
60
+ sys.exit(f'cannot read {sys.argv[2]}: {e}')
61
+ win = card.ventana(sys.argv[3])
62
+ print(card.rol_seguro(card.campo(texto, f'role.{win}')))
63
+ for campo in (f'runs.{win}', f'model.{win}', 'model', f'effort.{win}', 'effort'):
64
+ print(_campo(texto, campo))
65
+ return
66
+
67
+ # Several fields, one interpreter. The launcher asks for a window's model
68
+ # and effort — with the card default behind each — which was four processes
69
+ # re-parsing the same small file, per window, before tmux even attached.
70
+ if len(sys.argv) > 3 and sys.argv[1] == '--varios':
71
+ try:
72
+ with open(sys.argv[2], encoding='utf-8') as f:
73
+ texto = f.read()
74
+ except OSError as e:
75
+ sys.exit(f'cannot read {sys.argv[2]}: {e}')
76
+ for campo in sys.argv[3:]:
77
+ print(_campo(texto, campo))
78
+ return
34
79
  if len(sys.argv) < 3:
35
80
  sys.exit(
36
81
  'usage: read-card.py <card.md> <field> | --window <repo> | '
37
- '--actor-role <card.md> <actor>'
82
+ '--actor-role <card.md> <actor> | --varios <card.md> <field>... | '
83
+ '--ventana <card.md> <window>'
38
84
  )
39
85
  ruta, campo = sys.argv[1], sys.argv[2]
40
86
  try:
@@ -42,13 +88,7 @@ def main():
42
88
  texto = f.read()
43
89
  except OSError as e:
44
90
  sys.exit(f'cannot read {ruta}: {e}')
45
- valor = card.campo(texto, campo)
46
- # A bracketed value is a list everywhere else in this product — `repos:`,
47
- # `agents:`, `mounts.<agent>:` — and the shell wants one line. Deciding by
48
- # SHAPE rather than by a list of blessed field names is what stopped this
49
- # from having to be edited every time the card grew another list.
50
- esLista = valor.strip().startswith('[') and valor.strip().endswith(']')
51
- print(','.join(card.lista(valor)) if esLista else valor)
91
+ print(_campo(texto, campo))
52
92
 
53
93
 
54
94
  if __name__ == '__main__':