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
@@ -250,6 +250,18 @@ function startConsole(): void {
250
250
  output: process.stdout,
251
251
  prompt: 'city> ',
252
252
  });
253
+ // Say what this prompt is before anybody has to guess.
254
+ //
255
+ // Claude runs headless behind this gateway so that every turn crosses the
256
+ // bus and lands in the Hall — which is the whole point, and which also means
257
+ // the pane is NOT the Claude Code interface somebody was expecting. A bare
258
+ // `city>` after a wall of startup lines reads as "it failed to open"; it took
259
+ // exactly one person meeting it for the first time to prove that.
260
+ process.stdout.write(
261
+ `\n This window is ${options.actor}, talking to ${runtime} through the city.\n` +
262
+ ' Type here to give it work. The full conversation appears in the town hall.\n' +
263
+ ' /exit closes this window.\n\n',
264
+ );
253
265
  terminal.prompt();
254
266
  terminal.on('line', (line) => {
255
267
  const prompt = line.trim();
@@ -0,0 +1,66 @@
1
+ {
2
+ "protocol": "agents-city-key-transparency-root-chain/1",
3
+ "roots": [
4
+ {
5
+ "signed": {
6
+ "protocol": "agents-city-key-transparency-root/1",
7
+ "version": 1,
8
+ "environment": "sandbox",
9
+ "controlPlaneUrl": "https://agents-city-connect-sandbox.pages.dev",
10
+ "relayUrl": "wss://agents-city-relay-sandbox.bluebell.workers.dev/v1/connect",
11
+ "issuedAt": 1788063673370,
12
+ "expiresAt": 1819686073370,
13
+ "previousRootHash": null,
14
+ "keys": {
15
+ "agents-city-sandbox-operator-v1": {
16
+ "kty": "OKP",
17
+ "crv": "Ed25519",
18
+ "x": "ZIEWw4EUQ3qyoKVowZsl4nJuiVom7EwUMctI8NljFF8",
19
+ "ext": true
20
+ },
21
+ "agents-city-sandbox-root-v1": {
22
+ "kty": "OKP",
23
+ "crv": "Ed25519",
24
+ "x": "S8DifYMze6j2U-tRF_5tkjsXw5cmcFabt8B1X3ON1N8",
25
+ "ext": true
26
+ },
27
+ "agents-city-sandbox-witness-v1": {
28
+ "kty": "OKP",
29
+ "crv": "Ed25519",
30
+ "x": "fErzxNY6ZMfwH98E9F7jbn-tw_nOHVN3B3VodL62m68",
31
+ "ext": true
32
+ }
33
+ },
34
+ "roles": {
35
+ "root": {
36
+ "keyIds": [
37
+ "agents-city-sandbox-root-v1"
38
+ ],
39
+ "threshold": 1
40
+ },
41
+ "operator": {
42
+ "keyIds": [
43
+ "agents-city-sandbox-operator-v1"
44
+ ],
45
+ "threshold": 1
46
+ },
47
+ "witness": {
48
+ "keyIds": [
49
+ "agents-city-sandbox-witness-v1"
50
+ ],
51
+ "threshold": 1
52
+ }
53
+ },
54
+ "maximumHeadAgeMs": 300000,
55
+ "maximumWitnessLagMs": 30000
56
+ },
57
+ "signatures": [
58
+ {
59
+ "protocol": "agents-city-key-transparency-root-signature/1",
60
+ "keyId": "agents-city-sandbox-root-v1",
61
+ "signature": "llGO2Kr236CfD5JCvaMjvtwW4gG98s-p4gowrKNl_fPKJ9ywPI9m6R_DH9uTbLgt6eGyXiTKxB4QVDWHMAloAg"
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ }
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env python3
2
+ """What this product does to the CLIs you already had, on this machine.
3
+
4
+ The pitch is that Agents City orchestrates the CLIs people already run, with
5
+ the plugins, skills, MCP servers and settings they already configured. That is
6
+ a claim, and a claim about somebody else's machine is worth exactly as much as
7
+ the command that checks it. This is that command's engine.
8
+
9
+ It reads `plugin/channel/runtime/arnes.json` — the same declaration the
10
+ connectors take their policy values from, so the report cannot describe a
11
+ behaviour the code does not have — and fills it in with what is actually on
12
+ this disk: their real settings file, their real plugins, their real MCP
13
+ servers, their real model.
14
+
15
+ Three columns, and the distinction between them is the whole point:
16
+
17
+ el trato what we add or override. Short, deliberate, and the reason the
18
+ product works at all: without it the bus is not the only route
19
+ and the cage does not hold.
20
+ heredamos what we deliberately do NOT send, so their own CLI reads their
21
+ own configuration for it.
22
+ respetamos what loads untouched.
23
+ """
24
+
25
+ import json
26
+ import os
27
+
28
+ AQUI = os.path.dirname(os.path.abspath(__file__))
29
+ RAIZ = os.path.dirname(os.path.dirname(AQUI))
30
+ DECLARACION = os.path.join(RAIZ, "plugin", "channel", "runtime", "arnes.json")
31
+
32
+
33
+ def declaracion():
34
+ try:
35
+ with open(DECLARACION, encoding="utf-8") as f:
36
+ return json.load(f).get("motores", {})
37
+ except (OSError, ValueError):
38
+ return {}
39
+
40
+
41
+ def _expande(ruta):
42
+ return os.path.expanduser(ruta)
43
+
44
+
45
+ def _lee_json(ruta):
46
+ try:
47
+ with open(_expande(ruta), encoding="utf-8") as f:
48
+ return json.load(f)
49
+ except (OSError, ValueError):
50
+ return None
51
+
52
+
53
+ def _lee_toml_plano(ruta):
54
+ """Top-level `key = value` pairs, which is where the settings that matter
55
+ live. The same deliberately small reader the Codex connector uses, so the
56
+ two agree about what they can see."""
57
+ fuera = {}
58
+ try:
59
+ with open(_expande(ruta), encoding="utf-8") as f:
60
+ for linea in f:
61
+ linea = linea.strip()
62
+ if linea.startswith("["):
63
+ break
64
+ if not linea or linea.startswith("#") or "=" not in linea:
65
+ continue
66
+ clave, valor = linea.split("=", 1)
67
+ fuera[clave.strip()] = valor.strip().strip("\"'")
68
+ except OSError:
69
+ return {}
70
+ return fuera
71
+
72
+
73
+ def resume(valor):
74
+ """One of their values, as a line somebody can read.
75
+
76
+ A permissions block is forty rules and a nested dict; printing it raw turns
77
+ a report into a wall and buries the three lines that matter. Structure is
78
+ counted, scalars are shown, and a long string is cut — but never silently:
79
+ what is cut says so.
80
+ """
81
+ if isinstance(valor, dict):
82
+ cuantos = sum(len(v) if isinstance(v, list) else 1 for v in valor.values())
83
+ return f"{cuantos} entries"
84
+ if isinstance(valor, list):
85
+ return f"{len(valor)} entries"
86
+ texto = str(valor)
87
+ return texto if len(texto) <= 60 else texto[:57] + "…"
88
+
89
+
90
+ def _suyo(config):
91
+ """Their configuration, as far as we can honestly read it."""
92
+ for ruta in config:
93
+ if ruta.endswith(".toml"):
94
+ datos = _lee_toml_plano(ruta)
95
+ if datos:
96
+ return ruta, datos
97
+ else:
98
+ datos = _lee_json(ruta)
99
+ if datos:
100
+ return ruta, datos
101
+ return (config[0] if config else ""), {}
102
+
103
+
104
+ def _cuenta_claude(datos):
105
+ """What their Claude actually loads, counted rather than assumed."""
106
+ fuera = {}
107
+ plugins = datos.get("enabledPlugins")
108
+ if isinstance(plugins, dict):
109
+ fuera["plugins"] = sum(1 for v in plugins.values() if v)
110
+ permisos = datos.get("permissions")
111
+ if isinstance(permisos, dict):
112
+ reglas = sum(len(v) for v in permisos.values() if isinstance(v, list))
113
+ if reglas:
114
+ fuera["permission rules"] = reglas
115
+ mcp = _lee_json("~/.claude.json")
116
+ if isinstance(mcp, dict) and isinstance(mcp.get("mcpServers"), dict):
117
+ fuera["MCP servers"] = len(mcp["mcpServers"])
118
+ return fuera
119
+
120
+
121
+ def informe(instalado=None):
122
+ """The whole table, one entry per runtime, ready to print or to serve.
123
+
124
+ `instalado` is injected so the report can be built on a machine that has
125
+ none of them — a test, or a person reading the docs — without pretending
126
+ they are there.
127
+ """
128
+ if instalado is None:
129
+ import shutil
130
+
131
+ def instalado(binario):
132
+ return bool(shutil.which(binario))
133
+
134
+ fuera = []
135
+ for nombre, motor in sorted(declaracion().items()):
136
+ ruta, datos = _suyo(motor.get("config", []))
137
+ # A flat reader over a TOML file sees the top level and stops.
138
+ parcial = ruta.endswith(".toml") and bool(datos)
139
+ hereda = []
140
+ for h in motor.get("hereda", []):
141
+ valor = datos.get(h["suyo"])
142
+ hereda.append(
143
+ {
144
+ "clave": h["suyo"],
145
+ "cuando": h.get("cuando", ""),
146
+ "valor": "" if valor is None else resume(valor),
147
+ # Found, or merely not found HERE? This reader sees the top
148
+ # level of a TOML file and stops, so a key it did not find
149
+ # may be absent or may be inside a `[section]`. Saying "not
150
+ # set" would be the report inventing a fact about somebody
151
+ # else's machine.
152
+ "leido": h["suyo"] in datos,
153
+ }
154
+ )
155
+ avisos = []
156
+ for aviso in motor.get("avisa", []):
157
+ clave, _, esperado = aviso["cuando"].partition(" = ")
158
+ if esperado and str(datos.get(clave.strip(), "")) == esperado.strip():
159
+ avisos.append(aviso["dice"])
160
+ entrada = {
161
+ "motor": nombre,
162
+ "binario": motor.get("binario", nombre),
163
+ "instalado": instalado(motor.get("binario", nombre)),
164
+ "config": ruta,
165
+ "hay_config": bool(datos),
166
+ "trato": motor.get("trato", []),
167
+ "hereda": hereda,
168
+ "respeta": motor.get("respeta", []),
169
+ "avisos": avisos,
170
+ # A property of the FILE, not of each key read out of it.
171
+ "parcial": parcial,
172
+ }
173
+ if nombre == "claude" and datos:
174
+ entrada["cuenta"] = _cuenta_claude(datos)
175
+ fuera.append(entrada)
176
+ return fuera
177
+
178
+
179
+ def banderas(nombre):
180
+ """The declared deal for one runtime, as command-line flags.
181
+
182
+ Claude's half of the deal used to live as two shell literals in
183
+ `city-session.sh` while `arnes.json` declared the same three values — so
184
+ the drift guard, which only scans the connectors, passed **vacuously** for
185
+ the one runtime whose values were actually respelled somewhere else. The
186
+ launcher asks for them now, which makes the declaration true for all four
187
+ instead of two.
188
+
189
+ settings a key in the `--settings` layer Claude merges on top of theirs
190
+ flag a flag of its own, `--kebab-case-of-the-key value`
191
+ """
192
+ motor = declaracion().get(nombre, {})
193
+ ajustes, flags = {}, []
194
+ for t in motor.get("trato", []):
195
+ valor = t["valor"]
196
+ if t.get("rinde") == "settings":
197
+ ajustes[t["clave"]] = {"true": True, "false": False}.get(valor, valor)
198
+ elif t.get("rinde") == "flag":
199
+ guion = "".join("-" + c.lower() if c.isupper() else c for c in t["clave"])
200
+ flags.append(f"--{guion} {valor}")
201
+ partes = []
202
+ if ajustes:
203
+ partes.append("--settings " + json.dumps(ajustes, separators=(",", ":")))
204
+ partes += flags
205
+ return " ".join(partes)
206
+
207
+
208
+ #: Enough for the widest label in the report ("we inherit", "untouched").
209
+ ANCHO = 13
210
+
211
+
212
+ def _linea(etiqueta, texto):
213
+ return f" {etiqueta.ljust(ANCHO)}{texto}"
214
+
215
+
216
+ def imprime(entradas, di=print):
217
+ """The report, as somebody deciding whether to trust this would read it."""
218
+ di("\n What Agents City does to the CLIs you already have\n")
219
+ for e in entradas:
220
+ estado = "" if e["instalado"] else " (not installed here)"
221
+ di(f" {e['motor']}{estado}")
222
+ if not e["trato"]:
223
+ di(_linea("the deal", "nothing — it runs exactly as you configured it"))
224
+ for t in e["trato"]:
225
+ suyo = f" · yours wins ({t['suyo']})" if t.get("suyo") else ""
226
+ di(_linea("the deal", f"{t['clave']} = {resume(t['valor'])}{suyo}"))
227
+ di(_linea("", f" via {t['via']}"))
228
+ di(_linea("", f" because {t['porque']}"))
229
+ for h in e["hereda"]:
230
+ if h["valor"]:
231
+ valor = f" = {h['valor']}"
232
+ elif not e["hay_config"]:
233
+ valor = ""
234
+ elif not e["parcial"]:
235
+ valor = " (not set)"
236
+ else:
237
+ valor = " (not at the top level; this reader stops at [sections])"
238
+ di(_linea("we inherit", f"{h['clave']}{valor} · {h['cuando']}"))
239
+ if e["respeta"]:
240
+ di(_linea("untouched", ", ".join(e["respeta"])))
241
+ for clave, cuantos in (e.get("cuenta") or {}).items():
242
+ di(_linea("", f"{cuantos} {clave} on this machine"))
243
+ if e["hay_config"]:
244
+ di(_linea("read from", e["config"]))
245
+ for aviso in e["avisos"]:
246
+ di(_linea("careful", aviso))
247
+ di("")
248
+ di(" The deal is what makes the bus the only route and the cage hold.")
249
+ di(" CITY_CAGE=0 removes the cage. Everything else above is yours.\n")
250
+
251
+
252
+ def main(argv=None):
253
+ import sys
254
+
255
+ argv = sys.argv[1:] if argv is None else argv
256
+ # `arnes.py flags <runtime>` is what the launcher asks, so the deal it
257
+ # applies and the deal this reports are one answer.
258
+ if argv[:1] == ["flags"]:
259
+ print(banderas(argv[1] if len(argv) > 1 else ""))
260
+ return 0
261
+ entradas = informe()
262
+ if "--json" in argv:
263
+ json.dump(entradas, sys.stdout, ensure_ascii=False, indent=1)
264
+ print()
265
+ else:
266
+ imprime(entradas)
267
+ return 0
268
+
269
+
270
+ if __name__ == "__main__":
271
+ raise SystemExit(main())
@@ -86,7 +86,23 @@ SALTAR = {
86
86
 
87
87
  #: What counts as a document. Deliberately short: the question is "is there
88
88
  #: writing in here", not "can this folder be indexed".
89
- DOCUMENTOS = (".md", ".markdown", ".txt", ".rst", ".org", ".pdf", ".docx", ".doc", ".rtf")
89
+ #:
90
+ #: One answer, shared with `crecimiento`, which grows a knowledge agent's house
91
+ #: by counting the same files. Two lists here meant a visible contradiction: a
92
+ #: folder of `.adoc` was never offered as a knowledge house and yet grew one
93
+ #: once mounted by hand.
94
+ DOCUMENTOS = (
95
+ ".md",
96
+ ".markdown",
97
+ ".txt",
98
+ ".rst",
99
+ ".org",
100
+ ".adoc",
101
+ ".pdf",
102
+ ".doc",
103
+ ".docx",
104
+ ".rtf",
105
+ )
90
106
 
91
107
  #: A folder needs this many documents before it is worth offering. Two loose
92
108
  #: readmes are not a knowledge base.
@@ -350,17 +366,22 @@ def _lee():
350
366
  return sitios
351
367
 
352
368
 
353
- def caduco(dias=1):
369
+ #: How long the index is trusted. A day: long enough that nobody pays for the
370
+ #: crawl twice in a sitting, short enough that yesterday's clones show up.
371
+ VIDA = 86400
372
+
373
+
374
+ def caduco():
354
375
  try:
355
- return time.time() - os.path.getmtime(fichero_cache()) > dias * 86400
376
+ return time.time() - os.path.getmtime(fichero_cache()) > VIDA
356
377
  except OSError:
357
378
  return True
358
379
 
359
380
 
360
- def lugares(refrescar=False, dias=1):
381
+ def lugares(refrescar=False):
361
382
  """Every place, from the cache while it is fresh. The first crawl of a full
362
383
  home directory is not fast and nobody should pay for it twice."""
363
- if not refrescar and not caduco(dias):
384
+ if not refrescar and not caduco():
364
385
  guardado = _lee()
365
386
  if guardado is not None:
366
387
  return guardado
@@ -379,11 +400,26 @@ def repos(refrescar=False):
379
400
 
380
401
  def ruta_de(nombre):
381
402
  """One place's path by name, rebuilding the index once if it is not there —
382
- the repo somebody cloned a minute ago is exactly the one they came to use."""
383
- for intento in (False, True):
384
- for s in lugares(refrescar=intento):
385
- if s["nombre"] == nombre:
386
- return s["ruta"]
403
+ the repo somebody cloned a minute ago is exactly the one they came to use.
404
+
405
+ Once, though. The launcher resolves every name on a card, each in its own
406
+ process, so a card with three stale names used to mean three full crawls of
407
+ the home directory in a row: the rebuild triggered by the first name is not
408
+ going to make the second one appear. An index rebuilt seconds ago is
409
+ treated as the answer.
410
+ """
411
+ for s in lugares():
412
+ if s["nombre"] == nombre:
413
+ return s["ruta"]
414
+ try:
415
+ recien = time.time() - os.path.getmtime(fichero_cache()) < 30
416
+ except OSError:
417
+ recien = False
418
+ if recien:
419
+ return ""
420
+ for s in lugares(refrescar=True):
421
+ if s["nombre"] == nombre:
422
+ return s["ruta"]
387
423
  return ""
388
424
 
389
425
 
@@ -136,6 +136,11 @@ def sellados_por_tipo(casa):
136
136
  for f in SECRETOS_FICHERO:
137
137
  fuera[rutas.canonicaliza(os.path.join(casa, f))] = 'file'
138
138
  fuera[rutas.canonicaliza(os.path.join(ac, '.runtime', 'broker'))] = 'dir'
139
+ # Managed Roads use a device-held Ed25519 signing key and X25519 decryption
140
+ # key. Repo-agent windows never need either: the local hub is started by the
141
+ # human-facing connect command and owns the outbound session. Seal the whole
142
+ # state directory on both kernels, including before it exists.
143
+ fuera[rutas.canonicaliza(os.path.join(ac, '.runtime', 'connect'))] = 'dir'
139
144
  for f in ('credentials', 'credentials.toml'):
140
145
  fuera[rutas.canonicaliza(os.path.join(casa, '.cargo', f))] = 'file'
141
146
  # An owner's own denies: nothing declares their kind, so ask the disk and