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
|
@@ -100,11 +100,19 @@ if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
|
100
100
|
exec tmux attach -d -t "$SESSION"
|
|
101
101
|
fi
|
|
102
102
|
|
|
103
|
-
# Claude's
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
# Claude's half of the deal, asked for rather than respelled.
|
|
104
|
+
#
|
|
105
|
+
# `arnes.json` declares what this product adds to somebody else's CLI, and
|
|
106
|
+
# `agents-city doctor --config` prints that declaration as a promise about their
|
|
107
|
+
# machine. These two lines used to be a second spelling of it — which meant the
|
|
108
|
+
# drift guard, which reads the connectors, could not see the one runtime whose
|
|
109
|
+
# values live here. One `python3` in a script that already runs dozens buys the
|
|
110
|
+
# promise being true for all four runtimes instead of two.
|
|
111
|
+
#
|
|
112
|
+
# What it contains: Claude's native cross-session path closed and its peer tools
|
|
113
|
+
# denied, so the city bus is the only route; and the local yolo notice
|
|
114
|
+
# suppressed, which grants nothing.
|
|
115
|
+
CLAUDE_TRATO=" $(python3 "$(dirname "$0")/arnes.py" flags claude)"
|
|
108
116
|
YOLO_FLAG=""
|
|
109
117
|
[ "$YOLO" -eq 1 ] && YOLO_FLAG=" --dangerously-skip-permissions"
|
|
110
118
|
|
|
@@ -195,17 +203,82 @@ jaula_de() { # window, cwd, [broker token file], [colon-joined mount targets] -
|
|
|
195
203
|
# then the card's per-window key (`model.dbt:`), then the card's default
|
|
196
204
|
# (`model:`). Nothing set means no flag at all — the owner's Claude default,
|
|
197
205
|
# which is the right silence.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
# Used only by con_motor below, which is the single entry point: a window whose
|
|
207
|
+
# card says `model.dbt` must get it whichever of the six launch branches it
|
|
208
|
+
# lands in, and one of them used to give it nothing at all.
|
|
209
|
+
motor_de() { # $1 = window name, $2 = runtime (default claude) -> extra flags
|
|
210
|
+
local ventana="$1" cual="${2:-claude}" modelo esfuerzo salida="" cuatro
|
|
211
|
+
# Four fields, one interpreter — and none at all when the window loop has
|
|
212
|
+
# already read this window's card line. This used to be four `python3` starts
|
|
213
|
+
# each re-parsing the same small card, per window; a city with eighteen agents
|
|
214
|
+
# paid seventy-two of them before tmux attached.
|
|
215
|
+
if [ "$ventana" = "${CARTA_VENTANA:-}" ]; then
|
|
216
|
+
# Already read, by the loop that is launching this very window.
|
|
217
|
+
cuatro="$(printf '%s' "$CARTA_DATOS" | sed -n '3,6p')"
|
|
218
|
+
else
|
|
219
|
+
cuatro="$(python3 "$LEER" --varios "$FICHA" \
|
|
220
|
+
"model.$ventana" model "effort.$ventana" effort)"
|
|
221
|
+
fi
|
|
222
|
+
modelo="${CITY_MODEL:-$(printf '%s' "$cuatro" | sed -n 1p)}"
|
|
223
|
+
[ -z "$modelo" ] && modelo="$(printf '%s' "$cuatro" | sed -n 2p)"
|
|
224
|
+
esfuerzo="${CITY_EFFORT:-$(printf '%s' "$cuatro" | sed -n 3p)}"
|
|
225
|
+
[ -z "$esfuerzo" ] && esfuerzo="$(printf '%s' "$cuatro" | sed -n 4p)"
|
|
204
226
|
[ -n "$modelo" ] && salida=" --model $modelo"
|
|
205
|
-
|
|
227
|
+
# Only where it is actually read. Claude takes it as a CLI flag; Codex's
|
|
228
|
+
# gateway parses it out of the command and sends it with the turn. OpenCode
|
|
229
|
+
# and Kimi have no such setting, and writing a flag nothing reads is how a
|
|
230
|
+
# control ends up looking like it works.
|
|
231
|
+
if [ -n "$esfuerzo" ]; then
|
|
232
|
+
case "$cual" in claude|codex) salida="$salida --effort $esfuerzo" ;; esac
|
|
233
|
+
fi
|
|
206
234
|
printf '%s' "$salida"
|
|
207
235
|
}
|
|
208
236
|
|
|
237
|
+
# The card says which model a window runs, once, whatever CLI runs it. Claude
|
|
238
|
+
# takes these as flags; the native gateways parse the very same spelling out of
|
|
239
|
+
# the command string and send it with the turn — which is why one key on the
|
|
240
|
+
# card can mean the same thing for all four. A command that already carries the
|
|
241
|
+
# flag keeps it: somebody who wrote `runs.dbt: codex --model x` said what they
|
|
242
|
+
# meant, and a generic key must not overrule a specific sentence.
|
|
243
|
+
con_motor() { # $1 = window, $2 = runtime, $3 = the command
|
|
244
|
+
local ventana="$1" cual="$2" orden="$3" extra
|
|
245
|
+
extra="$(motor_de "$ventana" "$cual")"
|
|
246
|
+
case "$orden" in
|
|
247
|
+
*" --model "*|*" -m "*) extra="$(printf '%s' "$extra" | sed 's/ --model [^ ]*//')" ;;
|
|
248
|
+
esac
|
|
249
|
+
case "$orden" in
|
|
250
|
+
*" --effort "*) extra="$(printf '%s' "$extra" | sed 's/ --effort [^ ]*//')" ;;
|
|
251
|
+
esac
|
|
252
|
+
printf '%s%s' "$orden" "$extra"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# How a window's Claude is presented: its own interface, or the city's prompt.
|
|
256
|
+
#
|
|
257
|
+
# tui Claude Code as you run it by hand — your plugins, your statusline,
|
|
258
|
+
# your slash commands. It reaches the bus through the city plugin's
|
|
259
|
+
# SessionStart/UserPromptSubmit/Stop hooks, which report the same
|
|
260
|
+
# `conversation.*` events the gateway reports.
|
|
261
|
+
# gateway Claude headless behind the city gateway, driven from a `city>`
|
|
262
|
+
# prompt. The bus can PUSH work into it, which is what an agent
|
|
263
|
+
# house needs and a chair does not.
|
|
264
|
+
#
|
|
265
|
+
# The seat defaults to `tui` because the seat is where a person works by hand,
|
|
266
|
+
# and taking their own harness away to give them a bare prompt was the wrong
|
|
267
|
+
# trade. `ui.seat: gateway` on the card, or CITY_UI, puts the old shape back.
|
|
268
|
+
#
|
|
269
|
+
# Agent houses are NOT asked: a house exists to receive assignments from the
|
|
270
|
+
# bus, and the gateway is what makes that possible. The helper takes a window
|
|
271
|
+
# and a default because that is the honest shape of the question — but only the
|
|
272
|
+
# chair asks it, and the docs say only that.
|
|
273
|
+
ui_de() { # $1 = window, $2 = default -> tui | gateway
|
|
274
|
+
local elegido
|
|
275
|
+
elegido="${CITY_UI:-$(python3 "$LEER" "$FICHA" "ui.$1")}"
|
|
276
|
+
case "$elegido" in
|
|
277
|
+
tui|gateway) printf '%s' "$elegido" ;;
|
|
278
|
+
*) printf '%s' "$2" ;;
|
|
279
|
+
esac
|
|
280
|
+
}
|
|
281
|
+
|
|
209
282
|
runtime_de() { # configured command -> claude | codex | opencode | kimi | terminal | unknown
|
|
210
283
|
local raw="$1" first
|
|
211
284
|
case "$raw" in terminal:*) printf 'terminal'; return ;; esac
|
|
@@ -399,10 +472,52 @@ tmux set-environment -t "$SESSION" AGENTS_CITY_USER "$USUARIO" 2>/dev/null
|
|
|
399
472
|
tmux set-environment -t "$SESSION" CITY_ADDRESS "$ADDRESS" 2>/dev/null
|
|
400
473
|
tmux set-environment -t "$SESSION" CITY_SEAT_NAME "$SEAT_NAME" 2>/dev/null
|
|
401
474
|
|
|
475
|
+
# What every window is told about the city it belongs to.
|
|
476
|
+
#
|
|
477
|
+
# This was spelled out seven times, in seven three-hundred-column lines, and two
|
|
478
|
+
# of the copies existed only because the seat's non-Claude branches bypassed the
|
|
479
|
+
# helper that already had it. Adding one variable meant finding all seven.
|
|
480
|
+
#
|
|
481
|
+
# The chair and a house differ in exactly three keys: a house carries its
|
|
482
|
+
# operating role and has the remote-road variables blanked, because a road is
|
|
483
|
+
# the seat's to hold; the chair carries its bus agent instead.
|
|
484
|
+
entorno_de() { # $1 = actor ("seat" or a window name)
|
|
485
|
+
local actor="$1" propio
|
|
486
|
+
if [ "$actor" = seat ]; then
|
|
487
|
+
propio="CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE "
|
|
488
|
+
else
|
|
489
|
+
propio="CITY_BUS_ACTOR=$actor CITY_AGENT_ROLE=$ROL_REPO CITY_RUNTIME_KIND=repo CITY_BUS_URL= CITY_BUS_TOKEN= "
|
|
490
|
+
fi
|
|
491
|
+
printf '%s%s' "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME " "$propio"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# The chair's Claude, opened the way the card asks for.
|
|
495
|
+
#
|
|
496
|
+
# Both spellings carry the SAME flags. That matters: `--settings` closes
|
|
497
|
+
# Claude's own cross-session inbound and `--disallowed-tools` removes the peer
|
|
498
|
+
# tools, and those two are what make the city bus the only route between
|
|
499
|
+
# agents. A TUI without them would be a quieter product with a hole in it.
|
|
500
|
+
lanza_asiento_claude() { # $1 = the full claude command
|
|
501
|
+
local orden="$1" entorno
|
|
502
|
+
entorno="$(entorno_de seat)"
|
|
503
|
+
if [ "$(ui_de seat tui)" = tui ]; then
|
|
504
|
+
# Claude Code itself, in the pane. The city plugin is installed at user
|
|
505
|
+
# scope, so its hooks report this session's prompts and answers onto the
|
|
506
|
+
# bus exactly as the gateway would; the difference is that the city cannot
|
|
507
|
+
# push a prompt in, and instead types one through the registered fallback.
|
|
508
|
+
lanza "$SESSION:seat" seat "$EQUIPO" "$entorno${CLAUDE_AUTH_PREFIX}$orden"
|
|
509
|
+
"$RUNTIME" fallback seat "$SESSION:seat" claude
|
|
510
|
+
else
|
|
511
|
+
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
512
|
+
"$entorno${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$orden" "$SEAT_AUTO")"
|
|
513
|
+
fi
|
|
514
|
+
}
|
|
515
|
+
|
|
402
516
|
# The seat runs Claude unless the card says otherwise. `runs.seat` is the same key
|
|
403
517
|
# the repo windows use, and it makes a city with no Claude in it possible at all:
|
|
404
518
|
# what that seat gives up is the `/city:` commands, which are Claude's, and what it
|
|
405
519
|
# keeps is the folder, the identity, and everything the terminal does.
|
|
520
|
+
CARTA_VENTANA="" # set by the window loop below; the seat is not in it
|
|
406
521
|
SEAT_OTRO="$(python3 "$LEER" "$FICHA" "runs.seat")"
|
|
407
522
|
turno=0
|
|
408
523
|
[ -n "$SEAT_OTRO" ] && [ "$(runtime_de "$SEAT_OTRO")" != claude ] && turno=-1
|
|
@@ -411,25 +526,22 @@ if [ "$RUN_CLAUDE" -eq 1 ]; then
|
|
|
411
526
|
if [ -n "$SEAT_OTRO" ]; then
|
|
412
527
|
SEAT_RUNTIME="$(runtime_de "$SEAT_OTRO")"
|
|
413
528
|
if [ "$SEAT_RUNTIME" = claude ]; then
|
|
414
|
-
CLAUDE_SEAT="$SEAT_OTRO --name $SEAT_NAME
|
|
415
|
-
|
|
416
|
-
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE ${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$CLAUDE_SEAT" "$SEAT_AUTO")"
|
|
529
|
+
CLAUDE_SEAT="$(con_motor seat claude "$SEAT_OTRO --name $SEAT_NAME")$CLAUDE_TRATO$SEAT_YOLO_FLAG"
|
|
530
|
+
lanza_asiento_claude "$CLAUDE_SEAT"
|
|
417
531
|
elif [ "$SEAT_RUNTIME" = codex ] || [ "$SEAT_RUNTIME" = opencode ] || [ "$SEAT_RUNTIME" = kimi ]; then
|
|
418
532
|
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
419
|
-
"$(
|
|
533
|
+
"$(entorno_de seat)$(gateway_line seat "$EQUIPO" "$(con_motor seat "$SEAT_RUNTIME" "$SEAT_OTRO")" "$SEAT_AUTO")"
|
|
420
534
|
elif [ "$SEAT_RUNTIME" = terminal ]; then
|
|
421
535
|
SEAT_COMMAND="${SEAT_OTRO#terminal:}"
|
|
422
536
|
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
423
|
-
"$(
|
|
537
|
+
"$(entorno_de seat)$SEAT_COMMAND"
|
|
424
538
|
"$RUNTIME" fallback seat "$SESSION:seat" "${SEAT_COMMAND%% *}"
|
|
425
539
|
else
|
|
426
540
|
tmux send-keys -t "$SESSION:seat" \
|
|
427
541
|
"echo 'No native Agents City gateway for: $SEAT_OTRO. Use terminal:$SEAT_OTRO only if you explicitly accept terminal injection.'" C-m
|
|
428
542
|
fi
|
|
429
543
|
else
|
|
430
|
-
|
|
431
|
-
lanza "$SESSION:seat" seat "$EQUIPO" \
|
|
432
|
-
"$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME CITY_BUS_ACTOR=seat CITY_RUNTIME_KIND=seat CITY_BUS_AGENT=$AGENTE ${CLAUDE_AUTH_PREFIX}$(gateway_line seat "$EQUIPO" "$CLAUDE_SEAT" "$SEAT_AUTO")"
|
|
544
|
+
lanza_asiento_claude "$(con_motor seat claude "claude --name $SEAT_NAME")$CLAUDE_TRATO$SEAT_YOLO_FLAG"
|
|
433
545
|
fi
|
|
434
546
|
fi
|
|
435
547
|
|
|
@@ -463,7 +575,11 @@ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
|
463
575
|
# Professional specialty is separate from bus authority. Every repo actor is
|
|
464
576
|
# still a member; only `seat` is chair. A malformed or legacy value resolves
|
|
465
577
|
# to the explicit blank role in read-card.py.
|
|
466
|
-
|
|
578
|
+
# One read for this whole window: its role, what runs there, and the engine.
|
|
579
|
+
# Three interpreters per window used to re-parse the same card three times.
|
|
580
|
+
CARTA_VENTANA="$win"
|
|
581
|
+
CARTA_DATOS="$(python3 "$LEER" --ventana "$FICHA" "$win")"
|
|
582
|
+
ROL_REPO="$(printf '%s' "$CARTA_DATOS" | sed -n 1p)"
|
|
467
583
|
# This window's cage and, when the broker runs, its own single-repo token.
|
|
468
584
|
# The token travels as a file path, never on the command line, and the cage
|
|
469
585
|
# profile re-allows reading exactly that file and nothing else in the broker.
|
|
@@ -479,16 +595,16 @@ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
|
479
595
|
[ -n "$JAULA" ] && CAGE_RUNTIME_ENV="env CITY_OUTER_CAGE=1 "
|
|
480
596
|
tmux new-window -t "$SESSION" -n "$win" -c "$path"
|
|
481
597
|
if [ "$RUN_CLAUDE" -eq 1 ]; then
|
|
482
|
-
OTRO="$(
|
|
598
|
+
OTRO="$(printf '%s' "$CARTA_DATOS" | sed -n 2p)"
|
|
483
599
|
if [ -n "$OTRO" ]; then
|
|
484
600
|
KIND="$(runtime_de "$OTRO")"
|
|
485
601
|
if [ "$KIND" = claude ]; then
|
|
486
602
|
espera="$(retraso "$turno")"
|
|
487
603
|
turno=$((turno + 1))
|
|
488
604
|
if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
|
|
489
|
-
CLAUDE_REPO="$OTRO --name $SESSION-$win
|
|
605
|
+
CLAUDE_REPO="$(con_motor "$win" claude "$OTRO --name $SESSION-$win")$CLAUDE_TRATO$YOLO_FLAG"
|
|
490
606
|
lanza "$SESSION:$win" "$win" "$path" \
|
|
491
|
-
"$espera$(
|
|
607
|
+
"$espera$(entorno_de "$win")$BROKER_ENV$JAULA$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
|
|
492
608
|
elif [ "$KIND" = codex ] || [ "$KIND" = opencode ] || [ "$KIND" = kimi ]; then
|
|
493
609
|
# Native servers have their own credentials and do not share Claude's OAuth race.
|
|
494
610
|
# Codex stays outside the outer cage ON macOS ONLY: its node_repl MCP
|
|
@@ -505,11 +621,11 @@ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
|
505
621
|
RUNTIME_CAGE_ENV=""
|
|
506
622
|
fi
|
|
507
623
|
lanza "$SESSION:$win" "$win" "$path" \
|
|
508
|
-
"$(
|
|
624
|
+
"$(entorno_de "$win")$BROKER_ENV$RUNTIME_CAGE$RUNTIME_CAGE_ENV$(gateway_line "$win" "$path" "$(con_motor "$win" "$KIND" "$OTRO")")"
|
|
509
625
|
elif [ "$KIND" = terminal ]; then
|
|
510
626
|
FALLBACK_COMMAND="${OTRO#terminal:}"
|
|
511
627
|
lanza "$SESSION:$win" "$win" "$path" \
|
|
512
|
-
"$(
|
|
628
|
+
"$(entorno_de "$win")$BROKER_ENV$JAULA$FALLBACK_COMMAND"
|
|
513
629
|
"$RUNTIME" fallback "$win" "$SESSION:$win" "${FALLBACK_COMMAND%% *}"
|
|
514
630
|
else
|
|
515
631
|
tmux send-keys -t "$SESSION:$win" \
|
|
@@ -519,9 +635,9 @@ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
|
519
635
|
espera="$(retraso "$turno")"
|
|
520
636
|
turno=$((turno + 1))
|
|
521
637
|
if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
|
|
522
|
-
CLAUDE_REPO="claude --name $SESSION-$win
|
|
638
|
+
CLAUDE_REPO="$(con_motor "$win" claude "claude --name $SESSION-$win")$CLAUDE_TRATO$YOLO_FLAG"
|
|
523
639
|
lanza "$SESSION:$win" "$win" "$path" \
|
|
524
|
-
"$espera$(
|
|
640
|
+
"$espera$(entorno_de "$win")$BROKER_ENV${JAULA}$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
|
|
525
641
|
fi
|
|
526
642
|
fi
|
|
527
643
|
done
|
|
@@ -21,6 +21,7 @@ filesystem; the git-backed `code` counter is injected so this module needs no
|
|
|
21
21
|
network and stays trivially testable.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import busca # it owns what counts as a document
|
|
24
25
|
import os
|
|
25
26
|
import time
|
|
26
27
|
|
|
@@ -28,7 +29,9 @@ import deliberations
|
|
|
28
29
|
import workspace
|
|
29
30
|
|
|
30
31
|
#: Extensions that count as a knowledge document.
|
|
31
|
-
|
|
32
|
+
#: What a knowledge house grows on. `busca` owns the list, because the same
|
|
33
|
+
#: question decides which folders the picker offers as one in the first place.
|
|
34
|
+
DOC_EXT = busca.DOCUMENTOS
|
|
32
35
|
DIAS_RECIENTE = 30
|
|
33
36
|
_SEG_RECIENTE = DIAS_RECIENTE * 24 * 3600
|
|
34
37
|
|
package/plugin/scripts/demos.py
CHANGED
|
@@ -17,21 +17,25 @@ AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
|
17
17
|
RAIZ = os.path.dirname(os.path.dirname(AQUI))
|
|
18
18
|
DEMO = os.path.join(RAIZ, "demo")
|
|
19
19
|
|
|
20
|
-
#:
|
|
21
|
-
#:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
20
|
+
#: The one thing about a demo that lives nowhere else: the label for the field
|
|
21
|
+
#: it belongs to. Everything else — which stories exist, what each is called,
|
|
22
|
+
#: which city it happens in — is read from `stories.py` and from the fixture's
|
|
23
|
+
#: own `city.yml`, because those already own it. A table repeating them meant a
|
|
24
|
+
#: fourth story was invisible in the Hall until somebody remembered this dict,
|
|
25
|
+
#: and renaming a demo city made the shelf quietly lie.
|
|
26
|
+
ETIQUETAS = {"software": "Software", "medicina": "Healthcare", "legal": "Legal"}
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def carpeta():
|
|
30
30
|
return os.path.join(DEMO, "grabaciones")
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
"""The
|
|
33
|
+
def _historias():
|
|
34
|
+
"""The stories themselves, when the demo folder is installed.
|
|
35
|
+
|
|
36
|
+
An install can be trimmed. A Hall whose demo shelf is empty should say so
|
|
37
|
+
rather than fail to load, so this returns nothing rather than raising.
|
|
38
|
+
"""
|
|
35
39
|
import sys
|
|
36
40
|
|
|
37
41
|
if DEMO not in sys.path:
|
|
@@ -39,31 +43,39 @@ def _titulos():
|
|
|
39
43
|
try:
|
|
40
44
|
from stories import STORIES # noqa: PLC0415
|
|
41
45
|
|
|
42
|
-
return
|
|
46
|
+
return STORIES
|
|
43
47
|
except (ImportError, AttributeError):
|
|
44
48
|
return {}
|
|
45
49
|
|
|
46
50
|
|
|
51
|
+
def ficha(ident, eventos_=None):
|
|
52
|
+
"""One demo's card, built from one parse of its recording.
|
|
53
|
+
|
|
54
|
+
`eventos_` lets a caller that already has the events avoid reading the file
|
|
55
|
+
again — which is the difference between the shelf costing one pass per demo
|
|
56
|
+
and the endpoint costing four to serve one of them.
|
|
57
|
+
"""
|
|
58
|
+
import cities
|
|
59
|
+
|
|
60
|
+
historia = _historias().get(ident)
|
|
61
|
+
ruta = os.path.join(carpeta(), f"{ident}.jsonl")
|
|
62
|
+
if historia is None or not os.path.isfile(ruta):
|
|
63
|
+
return None
|
|
64
|
+
if eventos_ is None:
|
|
65
|
+
eventos_ = _carga(ruta)
|
|
66
|
+
return {
|
|
67
|
+
"id": ident,
|
|
68
|
+
"dominio": ETIQUETAS.get(ident, ident),
|
|
69
|
+
"ciudad": cities.nombre(os.path.join(DEMO, historia.get("city", ""))),
|
|
70
|
+
"titulo": historia.get("title", ident),
|
|
71
|
+
"turnos": len(eventos_),
|
|
72
|
+
"reparto": _reparto(eventos_),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
47
76
|
def catalogo():
|
|
48
77
|
"""Every demo with a recording on disk, in a stable order."""
|
|
49
|
-
|
|
50
|
-
fuera = []
|
|
51
|
-
for ident in sorted(DOMINIOS):
|
|
52
|
-
ruta = os.path.join(carpeta(), f"{ident}.jsonl")
|
|
53
|
-
if not os.path.isfile(ruta):
|
|
54
|
-
continue
|
|
55
|
-
dominio, ciudad = DOMINIOS[ident]
|
|
56
|
-
fuera.append(
|
|
57
|
-
{
|
|
58
|
-
"id": ident,
|
|
59
|
-
"dominio": dominio,
|
|
60
|
-
"ciudad": ciudad,
|
|
61
|
-
"titulo": titulos.get(ident, ident),
|
|
62
|
-
"turnos": _cuenta(ruta),
|
|
63
|
-
"reparto": _reparto(ruta),
|
|
64
|
-
}
|
|
65
|
-
)
|
|
66
|
-
return fuera
|
|
78
|
+
return [f for f in (ficha(i) for i in sorted(_historias())) if f]
|
|
67
79
|
|
|
68
80
|
|
|
69
81
|
def _lineas(ruta):
|
|
@@ -77,18 +89,27 @@ def _lineas(ruta):
|
|
|
77
89
|
return
|
|
78
90
|
|
|
79
91
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
def _carga(ruta):
|
|
93
|
+
"""One recording, read and parsed once.
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
The catalogue used to walk each file twice — once to count turns, once for
|
|
96
|
+
the cast — and then the endpoint parsed the chosen one a third time. Three
|
|
97
|
+
passes over the same small file to answer three questions about it.
|
|
98
|
+
"""
|
|
99
|
+
fuera = []
|
|
87
100
|
for linea in _lineas(ruta):
|
|
88
101
|
try:
|
|
89
|
-
|
|
102
|
+
fuera.append(json.loads(linea))
|
|
90
103
|
except json.JSONDecodeError:
|
|
91
104
|
continue
|
|
105
|
+
return fuera
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _reparto(eventos_):
|
|
109
|
+
"""Who speaks in this story, in the order they first do."""
|
|
110
|
+
visto = []
|
|
111
|
+
for evento in eventos_:
|
|
112
|
+
actor = evento.get("actor", "")
|
|
92
113
|
if actor and actor not in visto:
|
|
93
114
|
visto.append(actor)
|
|
94
115
|
return visto
|
|
@@ -96,12 +117,6 @@ def _reparto(ruta):
|
|
|
96
117
|
|
|
97
118
|
def eventos(ident):
|
|
98
119
|
"""One recording, as the list of activity events it holds."""
|
|
99
|
-
if ident not in
|
|
120
|
+
if ident not in _historias():
|
|
100
121
|
return []
|
|
101
|
-
|
|
102
|
-
for linea in _lineas(os.path.join(carpeta(), f"{ident}.jsonl")):
|
|
103
|
-
try:
|
|
104
|
-
fuera.append(json.loads(linea))
|
|
105
|
-
except json.JSONDecodeError:
|
|
106
|
-
continue
|
|
107
|
-
return fuera
|
|
122
|
+
return _carga(os.path.join(carpeta(), f"{ident}.jsonl"))
|
|
@@ -28,41 +28,56 @@ import sys
|
|
|
28
28
|
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
29
29
|
sys.path.insert(0, AQUI)
|
|
30
30
|
import atajos # noqa: E402 the desktop icons, one per city
|
|
31
|
+
import busca # noqa: E402 it owns where the disk index is cached
|
|
31
32
|
import cities # noqa: E402
|
|
32
33
|
import city_env # noqa: E402 where the bus keeps its folder
|
|
33
34
|
|
|
34
35
|
def _sitios():
|
|
35
36
|
"""Everything on disk that belongs to the product and to nothing else, as
|
|
36
37
|
(what it is, absolute path, whether it holds your cities)."""
|
|
37
|
-
casa = os.path.expanduser("~")
|
|
38
|
-
cache = os.environ.get("XDG_CACHE_HOME") or os.path.join(casa, ".cache")
|
|
39
38
|
return [
|
|
40
39
|
("your cities, their state and their backups", cities.raiz(), True),
|
|
41
40
|
("the list of which cities exist", os.path.dirname(cities.REGISTRO), False),
|
|
42
|
-
|
|
41
|
+
# Every entry here asks the module that owns the path. This one used to
|
|
42
|
+
# respell the cache rule, which meant moving the cache would silently
|
|
43
|
+
# leave it behind — the exact failure "everything it ever wrote" exists
|
|
44
|
+
# to prevent, and the kind nobody notices.
|
|
45
|
+
("the cached index of your disk", os.path.dirname(busca.fichero_cache()), False),
|
|
43
46
|
("the bus folder, its .env and its hooks", city_env.CANAL, False),
|
|
44
47
|
]
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
def _tamano(ruta):
|
|
48
|
-
"""How much is there, without following a single link out of it.
|
|
49
|
-
|
|
51
|
+
"""How much is there, without following a single link out of it.
|
|
52
|
+
|
|
53
|
+
Walked with `scandir`, whose entries already carry the type and size the
|
|
54
|
+
directory read fetched — `islink` plus `getsize` per file was two extra
|
|
55
|
+
syscalls each for an answer we were being handed.
|
|
56
|
+
"""
|
|
57
|
+
if os.path.islink(ruta):
|
|
50
58
|
return 0
|
|
51
|
-
if os.path.
|
|
52
|
-
return
|
|
59
|
+
if os.path.isfile(ruta):
|
|
60
|
+
return os.path.getsize(ruta)
|
|
53
61
|
total = 0
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
pila = [ruta]
|
|
63
|
+
while pila:
|
|
64
|
+
try:
|
|
65
|
+
with os.scandir(pila.pop()) as it:
|
|
66
|
+
for e in it:
|
|
67
|
+
try:
|
|
68
|
+
if e.is_symlink():
|
|
69
|
+
# A symlinked directory is somebody's repository. It
|
|
70
|
+
# is not ours to measure, and certainly not ours to
|
|
71
|
+
# delete.
|
|
72
|
+
continue
|
|
73
|
+
if e.is_dir(follow_symlinks=False):
|
|
74
|
+
pila.append(e.path)
|
|
75
|
+
else:
|
|
76
|
+
total += e.stat(follow_symlinks=False).st_size
|
|
77
|
+
except OSError:
|
|
78
|
+
continue
|
|
79
|
+
except OSError:
|
|
80
|
+
continue
|
|
66
81
|
return total
|
|
67
82
|
|
|
68
83
|
|
|
@@ -76,9 +91,9 @@ def _tamano_legible(n):
|
|
|
76
91
|
return f"{valor:.0f} {unidades[i]}" if i == 0 else f"{valor:.1f} {unidades[i]}"
|
|
77
92
|
|
|
78
93
|
|
|
79
|
-
def plan(
|
|
94
|
+
def plan(conservar_ciudades=False, con_npm=False):
|
|
80
95
|
"""What would be removed, in the order it would be removed."""
|
|
81
|
-
usuario =
|
|
96
|
+
usuario = cities.usuario_actual()
|
|
82
97
|
pasos = [{"que": "stop every city session, hall and map this product started",
|
|
83
98
|
"clase": "procesos"}]
|
|
84
99
|
for datos in [c["ruta"] for c in cities.lista(usuario)]:
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""What happened, written down, so a failure on somebody else's machine is not a
|
|
3
|
+
story they have to remember.
|
|
4
|
+
|
|
5
|
+
The Hall had no log at all. It answered the browser with an error, the browser
|
|
6
|
+
put a toast on screen, the toast went away, and there was nothing left. When
|
|
7
|
+
somebody said "it says the name is empty and the name is right there", the only
|
|
8
|
+
honest answer was "reproduce it for me" — which is asking a person to debug
|
|
9
|
+
their own bug report.
|
|
10
|
+
|
|
11
|
+
So: one file, always written, per city.
|
|
12
|
+
|
|
13
|
+
~/.agents-city/.runtime/bus/<city>/hall.jsonl
|
|
14
|
+
|
|
15
|
+
One JSON object per line, oldest first. The browser writes into the same file
|
|
16
|
+
as the server, because half of these failures happen in the browser and a log
|
|
17
|
+
that stops at the network boundary tells half a story.
|
|
18
|
+
|
|
19
|
+
Two things it must never do. It must not grow without bound — it rotates at two
|
|
20
|
+
megabytes and keeps one previous file, which is enough to see what happened and
|
|
21
|
+
not enough to matter. And it must not record a secret: `agents-city doctor
|
|
22
|
+
--report` exists so somebody can send this to a stranger, and a log that has to
|
|
23
|
+
be read before it is sent is a log nobody sends.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
#: Rotate here. Small enough that the file is readable, large enough to hold a
|
|
32
|
+
#: session's worth of a person clicking around.
|
|
33
|
+
LIMITE = 2 * 1024 * 1024
|
|
34
|
+
|
|
35
|
+
#: Anything whose NAME says it is a credential. Matched on the key, because the
|
|
36
|
+
#: value of a token looks like the value of an id.
|
|
37
|
+
SECRETO = re.compile(
|
|
38
|
+
r"pase|token|secret|password|passwd|authorization|cookie|api[_-]?key|credential",
|
|
39
|
+
re.I,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
#: And anything shaped like one wherever it turns up: a long unbroken run of
|
|
43
|
+
#: hex or base64. Slashes are NOT part of it — a temp directory is a long run
|
|
44
|
+
#: of characters with slashes in it, and redacting `/var/folders/...` turned a
|
|
45
|
+
#: useful error message into `[redacted]` while protecting nothing.
|
|
46
|
+
PARECE_CLAVE = re.compile(r"(?<![\w./-])[A-Za-z0-9+_-]{32,}={0,2}(?![\w./-])")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ruta(datos):
|
|
50
|
+
import runtime_processes
|
|
51
|
+
|
|
52
|
+
return os.path.join(runtime_processes.ruta(datos), "hall.jsonl")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def limpia(valor, profundidad=0):
|
|
56
|
+
"""The same value with anything that looks like a credential taken out.
|
|
57
|
+
|
|
58
|
+
Applied on the way IN, not on the way out: a secret that reaches the file
|
|
59
|
+
has already been written to somebody's disk, and the promise this makes is
|
|
60
|
+
that the file is safe to attach to an issue without reading it first.
|
|
61
|
+
"""
|
|
62
|
+
if profundidad > 6:
|
|
63
|
+
return "…"
|
|
64
|
+
if isinstance(valor, dict):
|
|
65
|
+
return {
|
|
66
|
+
k: ("[redacted]" if SECRETO.search(str(k)) else limpia(v, profundidad + 1))
|
|
67
|
+
for k, v in list(valor.items())[:40]
|
|
68
|
+
}
|
|
69
|
+
if isinstance(valor, (list, tuple)):
|
|
70
|
+
return [limpia(v, profundidad + 1) for v in valor[:40]]
|
|
71
|
+
if isinstance(valor, str):
|
|
72
|
+
recortado = valor if len(valor) <= 500 else valor[:497] + "…"
|
|
73
|
+
return PARECE_CLAVE.sub("[redacted]", recortado)
|
|
74
|
+
return valor
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _rota(fichero):
|
|
78
|
+
try:
|
|
79
|
+
if os.path.getsize(fichero) < LIMITE:
|
|
80
|
+
return
|
|
81
|
+
except OSError:
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
os.replace(fichero, fichero + ".1")
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def apunta(datos, tipo, **campos):
|
|
90
|
+
"""One line. Never raises: a log that can break the thing it is logging is
|
|
91
|
+
worse than no log, and this is called from inside request handlers."""
|
|
92
|
+
try:
|
|
93
|
+
fichero = ruta(datos)
|
|
94
|
+
os.makedirs(os.path.dirname(fichero), exist_ok=True)
|
|
95
|
+
_rota(fichero)
|
|
96
|
+
linea = {"t": time.strftime("%Y-%m-%dT%H:%M:%S"), "tipo": tipo}
|
|
97
|
+
linea.update(limpia(campos))
|
|
98
|
+
with open(fichero, "a", encoding="utf-8") as f:
|
|
99
|
+
f.write(json.dumps(linea, ensure_ascii=False) + "\n")
|
|
100
|
+
except (OSError, TypeError, ValueError):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def lee(datos, cuantas=200):
|
|
105
|
+
"""The last `cuantas` entries, oldest first, across the rotation."""
|
|
106
|
+
fuera = []
|
|
107
|
+
for fichero in (ruta(datos) + ".1", ruta(datos)):
|
|
108
|
+
try:
|
|
109
|
+
with open(fichero, encoding="utf-8") as f:
|
|
110
|
+
fuera.extend(l.strip() for l in f if l.strip())
|
|
111
|
+
except OSError:
|
|
112
|
+
continue
|
|
113
|
+
salida = []
|
|
114
|
+
for linea in fuera[-cuantas:]:
|
|
115
|
+
try:
|
|
116
|
+
salida.append(json.loads(linea))
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
salida.append({"tipo": "unreadable", "linea": linea[:200]})
|
|
119
|
+
return salida
|