agents-city 0.3.0 → 0.5.1

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 (107) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.es.md +294 -16
  3. package/README.md +291 -16
  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 +5 -0
  8. package/bin/connect +5 -0
  9. package/bin/hall.html +208 -22
  10. package/bin/navegador.mjs +203 -13
  11. package/bin/serve.py +176 -111
  12. package/bin/test +18 -4
  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-demo.py +67 -0
  21. package/bin/test-desinstala.py +170 -0
  22. package/bin/test-doctor.py +31 -0
  23. package/bin/test-i18n.py +189 -0
  24. package/bin/test-navegador.py +54 -0
  25. package/bin/test-seat.py +78 -20
  26. package/bin/test-security.py +2 -0
  27. package/bin/test-serve.py +414 -2
  28. package/bin/uninstall +5 -0
  29. package/city/web/dist/city.js +65 -65
  30. package/city/web/dist/index.html +18 -5
  31. package/city/web/dist-hall/hall.js +1566 -486
  32. package/city/web/index.html +17 -4
  33. package/city/web/src/bienvenida.ts +48 -341
  34. package/city/web/src/casa.ts +324 -0
  35. package/city/web/src/demo.ts +277 -0
  36. package/city/web/src/dialogo.ts +178 -0
  37. package/city/web/src/es.ts +281 -25
  38. package/city/web/src/explorador.ts +216 -0
  39. package/city/web/src/hall.ts +612 -139
  40. package/city/web/src/idioma.ts +21 -1
  41. package/city/web/src/main.ts +28 -5
  42. package/city/web/src/motores.ts +48 -1
  43. package/city/web/src/vista.ts +58 -0
  44. package/demo/graba.py +127 -0
  45. package/demo/grabaciones/legal.jsonl +22 -0
  46. package/demo/grabaciones/medicina.jsonl +22 -0
  47. package/demo/grabaciones/software.jsonl +22 -0
  48. package/docs/managed-connect.md +237 -0
  49. package/docs/security.md +41 -2
  50. package/package.json +8 -4
  51. package/plugin/.claude-plugin/plugin.json +1 -1
  52. package/plugin/channel/adapter-prompts.ts +10 -0
  53. package/plugin/channel/adapter.js +78 -31
  54. package/plugin/channel/agents_city_hybrid_crypto_bg.wasm +0 -0
  55. package/plugin/channel/bus.js +109 -65
  56. package/plugin/channel/bus.ts +11 -1
  57. package/plugin/channel/client.js +67 -30
  58. package/plugin/channel/delivery-queue.ts +160 -21
  59. package/plugin/channel/hub/remote-roads.ts +32 -1
  60. package/plugin/channel/hub/road-controller.ts +116 -16
  61. package/plugin/channel/kinsh_vodozemac_wasm_bg.wasm +0 -0
  62. package/plugin/channel/licenses/CONNECT_CLIENT_THIRD_PARTY_NOTICES.md +21 -0
  63. package/plugin/channel/licenses/HYBRID_CRYPTO_THIRD_PARTY_NOTICES.md +12 -0
  64. package/plugin/channel/licenses/hybrid-crypto-Apache-2.0.txt +201 -0
  65. package/plugin/channel/licenses/keyring-MIT.txt +21 -0
  66. package/plugin/channel/licenses/vodozemac-Apache-2.0.txt +201 -0
  67. package/plugin/channel/local-hub.js +2080 -97
  68. package/plugin/channel/local-hub.ts +5 -4
  69. package/plugin/channel/managed-connect/bridge.ts +208 -0
  70. package/plugin/channel/managed-connect/cli.ts +416 -0
  71. package/plugin/channel/managed-connect/device.ts +15 -0
  72. package/plugin/channel/managed-connect/local-cities.ts +94 -0
  73. package/plugin/channel/managed-connect/person-message.ts +74 -0
  74. package/plugin/channel/managed-connect/reception-bridge.ts +341 -0
  75. package/plugin/channel/managed-connect/relay-session.ts +7 -0
  76. package/plugin/channel/managed-connect/storage.ts +653 -0
  77. package/plugin/channel/managed-connect/transport.ts +87 -0
  78. package/plugin/channel/managed-connect-cli.js +4777 -0
  79. package/plugin/channel/managed-connect-cli.ts +7 -0
  80. package/plugin/channel/managed-connect-client.d.ts +261 -0
  81. package/plugin/channel/managed-connect-client.js +6572 -0
  82. package/plugin/channel/managed-connect-client.manifest.json +38 -0
  83. package/plugin/channel/package-lock.json +123 -105
  84. package/plugin/channel/package.json +4 -4
  85. package/plugin/channel/protocol.ts +4 -0
  86. package/plugin/channel/reception.ts +896 -0
  87. package/plugin/channel/road-cli.ts +1 -1
  88. package/plugin/channel/runtime/arnes.json +189 -0
  89. package/plugin/channel/runtime/arnes.ts +47 -0
  90. package/plugin/channel/runtime/claude.ts +18 -0
  91. package/plugin/channel/runtime/codex-config.ts +51 -1
  92. package/plugin/channel/runtime/codex.ts +31 -11
  93. package/plugin/channel/runtime/kimi.ts +6 -4
  94. package/plugin/channel/runtime-files.ts +39 -5
  95. package/plugin/channel/runtime-gateway.js +426 -100
  96. package/plugin/channel/runtime-gateway.ts +12 -0
  97. package/plugin/channel/trust/agents-city-sandbox-roots.json +66 -0
  98. package/plugin/scripts/arnes.py +271 -0
  99. package/plugin/scripts/busca.py +46 -10
  100. package/plugin/scripts/cage.py +5 -0
  101. package/plugin/scripts/city-session.sh +144 -28
  102. package/plugin/scripts/crecimiento.py +4 -1
  103. package/plugin/scripts/demos.py +122 -0
  104. package/plugin/scripts/desinstala.py +237 -0
  105. package/plugin/scripts/doctor.py +37 -15
  106. package/plugin/scripts/read-card.py +48 -8
  107. package/plugin/scripts/reception.py +665 -0
package/README.md CHANGED
@@ -104,7 +104,7 @@ This is `0.x` on purpose: the commands are usable today, and the file formats
104
104
  and APIs can still change between minor versions. Nothing here pretends to be
105
105
  frozen yet.
106
106
 
107
- You need Node.js 22+, Python 3 and tmux; the
107
+ You need Node.js 22.13+, Python 3 and tmux; the
108
108
  [requirements table](#base-requirements) has the details, and `agents-city seat`
109
109
  offers to install tmux when it is missing. Nothing is installed system-wide
110
110
  beyond the npm global folder of your active Node installation.
@@ -160,7 +160,7 @@ agents-city --version
160
160
 
161
161
  | Requirement | Used for |
162
162
  |---|---|
163
- | Node.js 22 or later | npm package, WebSocket bus, and frontends |
163
+ | Node.js 22.13 or later | npm package, WebSocket bus, local reception, and frontends |
164
164
  | npm | installation and packaging |
165
165
  | Python 3 | Hall, onboarding, cities, maps, and utilities |
166
166
  | bash | sessions and launchers |
@@ -486,6 +486,7 @@ agents-city setup
486
486
  agents-city seat
487
487
  agents-city cities
488
488
  agents-city road
489
+ agents-city connect
489
490
  agents-city bus
490
491
  agents-city committee
491
492
  agents-city agents
@@ -550,12 +551,44 @@ spectator token rotates with the hub, accepts only an origin on this computer,
550
551
  and is read-only: the browser cannot direct the committee. `Ctrl-c` stops the
551
552
  Hall.
552
553
 
554
+ **Demos** in the rail plays a whole committee without setting anything up: one
555
+ story per work domain — a studio, a clinic, a law firm — with play, pause,
556
+ replay and speed. What it plays are *recordings*: `demo/graba.py` runs each story
557
+ over the real local bus, through the real committee state machine, and keeps the
558
+ exact event stream a spectator saw; the Hall replays those events through the
559
+ same renderer the live rail uses. It says so on screen, because a demo that
560
+ pretends to be live is the one kind this product must not ship. To run one live
561
+ in a terminal instead: `agents-city demo --domain software`.
562
+
563
+ Regenerate the recordings after editing `demo/stories.py`:
564
+
565
+ ```bash
566
+ demo/graba.py # every story
567
+ demo/graba.py medicina # just one
568
+ ```
569
+
570
+ The demo suite fails when a recording no longer matches the story it claims to
571
+ be, so a stale one is a red build rather than a browser quietly playing last
572
+ month's committee.
573
+
553
574
  Two buttons sit under the brand: **day/night**, and **ES/EN**. The Hall speaks
554
575
  Spanish and English, starting in the browser's own language and remembering an
555
576
  explicit choice. Translations are keyed by the English sentence, so anything not
556
577
  yet translated falls back to readable English rather than to an identifier — new
557
578
  strings are never blocked on a translation pass.
558
579
 
580
+ Coverage is a test, not a habit. `bin/test-i18n.py` reads the render paths, pulls
581
+ out every English sentence a person will see, and fails when one has no Spanish —
582
+ so a new view cannot quietly ship untranslated, which is how coverage had drifted
583
+ to about 40% before anybody noticed.
584
+
585
+ The obvious mechanism, sweeping the rendered DOM and translating what matches a
586
+ key, is deliberately **not** what this does. At DOM time there is no way to tell
587
+ a sentence this product wrote from a city or agent name somebody typed, so a
588
+ person whose city is called `Overview` would watch it rename itself. The
589
+ distinction only exists in the source, between a literal and an interpolation,
590
+ and that is where the check is made: anything holding a `${}` is skipped.
591
+
559
592
  ### `agents-city setup`
560
593
 
561
594
  Creates or selects a city and opens the Hall; `--tui` hands the flow to `seat`.
@@ -661,6 +694,62 @@ agents-city road disconnect product <remote-city-id>
661
694
  A city cannot connect to itself. Each machine must independently accept the
662
695
  other remote invitation.
663
696
 
697
+ ### `agents-city connect`
698
+
699
+ Pairs this computer with a managed Road service. It does not create a
700
+ connection unilaterally: both people approve it in the service, and the
701
+ recipient sees the sender in their private human reception without exposing a
702
+ city catalogue. The public client implements protocol v4; the hosted service is
703
+ outside this repository and is not production-enabled or independently audited.
704
+
705
+ ```bash
706
+ agents-city connect --service https://connect.example.com --trust-file roots.json
707
+ agents-city connect --city product
708
+ agents-city connect --all
709
+ agents-city connect status
710
+ agents-city connect roads
711
+ ```
712
+
713
+ The command generates Ed25519/X25519, Olm and signed ML-KEM-768 material on this
714
+ computer, prints a one-use PASCO and opens the browser for approval. Only public
715
+ material is uploaded. Private keys, ratchet state, ML-KEM seeds and retry data
716
+ are encrypted in `~/.agents-city/.runtime/connect/vault/`; the wrapping key
717
+ stays in macOS Keychain, Windows Credential Manager or Linux Secret Service.
718
+ The client fails closed if that keyring is unavailable. The vault is sealed from
719
+ repo-agent windows on macOS and Linux.
720
+
721
+ The signed root chain supplied through `--trust-file` is mandatory for first
722
+ pairing with a non-development service. The client persists its last accepted
723
+ version. A later root must continue from that exact local root and carry enough
724
+ signatures from both the old and new offline authorities; skipped versions,
725
+ rollback, expiry and silent operator/witness replacement are rejected. Protocol
726
+ v4 then verifies the peer through key transparency, protects the first Olm
727
+ message with hybrid X25519 + ML-KEM-768, and uses the Olm Double Ratchet. Normal
728
+ sealed submissions omit sender, device, city and Road identity from the outer
729
+ request. This does not hide IP address, timing or padded size from Cloudflare,
730
+ and later ratchet steps are classical.
731
+
732
+ The package includes a public root only for the exact managed sandbox origin;
733
+ self-hosted services still require their reviewed `--trust-file`. A root
734
+ returned by the service is never accepted as a first pin.
735
+
736
+ `--city` chooses a local hub that can keep the computer's reception bridge
737
+ alive; it is not a recipient selector and is never disclosed to the other
738
+ person. Exactly one hub per computer holds the lease and one outbound encrypted
739
+ session; no public port is opened. Use `--service URL` or
740
+ `AGENTS_CITY_CONNECT_URL` for a pilot endpoint. The hosted server is not part of
741
+ this Apache repository; the auditable client and wire protocol are.
742
+
743
+ `agents-city connect roads` prints a connected person's name for a person Road,
744
+ not the opaque `rx-*` transport endpoints. In the Hall, every incoming message
745
+ waits for manual review by default. The owner may route it to one or more local
746
+ cities, reject it with a reason, or explicitly enable the deterministic Auto
747
+ router. Auto routes only one unique low-risk rule match; ambiguous, unmatched,
748
+ prompt-like, secret-seeking, or command-like text remains in the human queue.
749
+
750
+ See [docs/managed-connect.md](docs/managed-connect.md) for the exact key,
751
+ envelope, encryption, ACK, revocation and threat-model contract.
752
+
664
753
  ### `agents-city bus`
665
754
 
666
755
  Operates messages between seats over declared roads.
@@ -675,7 +764,7 @@ agents-city bus send '*' "Notice for every connected city"
675
764
  | Subcommand | Effect |
676
765
  |---|---|
677
766
  | `roster` | return roads and known online presence |
678
- | `inbox` | return and consume pending inbox; append-only history remains |
767
+ | `inbox` | return and consume the next approved batch of up to 20; managed text is unavailable until the owner routes it in the Hall |
679
768
  | `send owner/city TEXT` | send to one allowed destination |
680
769
  | `send '*' TEXT` | send to all roads; requires at least one |
681
770
 
@@ -1530,9 +1619,28 @@ AGENTS_CITY_DATA="$HOME/.agents-city/$CITY_OWNER/product" \
1530
1619
  Inside a normal session, you do not need to set `AGENTS_CITY_DATA`; it is already
1531
1620
  injected into each window. The example makes it explicit for an outside terminal.
1532
1621
 
1533
- ### Case 10: connect cities belonging to different machines or people
1622
+ ### Case 10: connect two people on different machines
1623
+
1624
+ With a managed Road operator, each person pairs a computer. `--city` chooses the
1625
+ local hub that will start the owner-level reception bridge; it does not reveal
1626
+ that city or give the other person direct access to it:
1627
+
1628
+ ```bash
1629
+ agents-city connect --city product --service https://connect.example.com --trust-file roots.json
1630
+ agents-city connect --city research --service https://connect.example.com --trust-file roots.json
1631
+ ```
1632
+
1633
+ One person requests the connection in that service and the other accepts it.
1634
+ The clients learn the active bilateral person Road over their authenticated
1635
+ relay sessions; neither side exchanges a shared bus token, exposes a local
1636
+ port, or receives the other person's city catalogue. Incoming text first stops
1637
+ in the human reception. The recipient decides which local city or cities may
1638
+ read it, or lets the optional fail-closed rule router decide when one match is
1639
+ unambiguous. The public client contract is documented in
1640
+ [docs/managed-connect.md](docs/managed-connect.md).
1534
1641
 
1535
- On machine A:
1642
+ To self-host the existing token-based remote transport instead, exchange the
1643
+ public city invitations manually. On machine A:
1536
1644
 
1537
1645
  ```bash
1538
1646
  agents-city road invite product > product.invitation.json
@@ -1760,12 +1868,29 @@ The local hub keeps ephemeral state separate from readable configuration:
1760
1868
  ├── road-queue/*.json
1761
1869
  ├── road-inbox/*.json
1762
1870
  └── road-history.jsonl
1871
+
1872
+ ~/.agents-city/.runtime/reception/
1873
+ └── reception.sqlite3 # owner quarantine shared by local cities
1763
1874
  ```
1764
1875
 
1765
1876
  Credentials and runtime files are created with private permissions. Outboxes let
1766
1877
  an actor reconnect without losing an already accepted task; its ACK removes the
1767
- pending item. Current limits are 200 pending items per queue and a 72-hour
1768
- message lifetime. `bus inbox` consumes `road-inbox`, not append-only history.
1878
+ pending item. Actor outboxes and the local retry queue admit 200 pending items;
1879
+ the Road inbox admits 500 by default and returns at most 20 oldest items per
1880
+ read. Managed E2EE text first enters the separate owner reception: no city or
1881
+ model can consume it until a person rejects it or routes it to one or more
1882
+ cities in the Hall. A routed burst creates one coalesced seat wake-up rather
1883
+ than one model turn per message, and every native runtime runs at most one turn
1884
+ at a time. Full queues apply backpressure instead of silently deleting an older
1885
+ item. Message lifetime is 72 hours. `bus inbox` consumes approved `road-inbox`,
1886
+ not reception quarantine or append-only history.
1887
+
1888
+ Relay throughput is not answer throughput. For one city, safe semantic capacity
1889
+ is approximately grouped requests per turn divided by turn duration. The local
1890
+ regression drains 100 Road messages in five exact batches of 20 after one
1891
+ content-free wake-up; a separate 20-request slow-runtime test proves model
1892
+ concurrency stays at one and the durable backlog drains without loss. A sender's
1893
+ `queued` result never means read or answered.
1769
1894
 
1770
1895
  ### Configurable variables
1771
1896
 
@@ -1787,6 +1912,12 @@ message lifetime. `bus inbox` consumes `road-inbox`, not append-only history.
1787
1912
  | `CITY_HOOKS` | `city` | `everywhere` runs the conscience hooks in every Claude session, not only city runtimes |
1788
1913
  | `CITY_DESKTOP` | `~/Desktop`, or the Windows desktop under WSL | where `agents-city shortcut` writes |
1789
1914
  | `CITY_CAGE` | `1` | `0` launches every window uncaged |
1915
+ | `CITY_ROAD_INBOX_MAX_PENDING` | `500` | local Road inbox capacity, from 20 to 10,000; a full inbox applies backpressure |
1916
+ | `CITY_ROAD_INBOX_WAKE_INTERVAL_MS` | `300000` | minimum interval between coalesced backlog wake-ups, from 30 seconds to 1 hour |
1917
+ | `CITY_RECEPTION_MAX_PENDING` | `10000` | owner-level pending remote messages before relay backpressure, from 100 to 100,000 |
1918
+ | `CITY_RECEPTION_MAX_BYTES` | `67108864` | total pending plaintext bytes in private local reception, from 1 MiB to 512 MiB |
1919
+ | `CITY_RECEPTION_PENDING_DAYS` | `30` | undecided local-message retention, from 1 to 90 days |
1920
+ | `CITY_RECEPTION_DELIVERY_INTERVAL_MS` | `1000` | how often a city bus claims human-approved routes, from 250 ms to 30 seconds |
1790
1921
  | `CITY_CAGE_DENY` | empty | extra colon-separated paths to seal |
1791
1922
  | `CITY_CAGE_ALLOW_WRITE` | empty | extra colon-separated paths to keep writable |
1792
1923
  | `CITY_UPDATE_CHECK` | `1` | `0` never asks npm whether a newer version exists |
@@ -1819,9 +1950,17 @@ CITY_SETTLE=0 CITY_STAGGER=0 agents-city seat --city product
1819
1950
 
1820
1951
  It reads `.git/config` and `HEAD` directly rather than running `git` once per
1821
1952
  repository, so a full scan finishes while somebody is looking at the screen, and
1822
- it runs anywhere Python does — macOS, Linux and Windows alike. The guide and the
1823
- Hall search this index; `plugin/scripts/find-repos.sh` is a thin shim over it for
1824
- shell callers, printing the git half only.
1953
+ it runs anywhere Python does — macOS, Linux and Windows alike. This index is what
1954
+ the **terminal** uses: `seat --repos`, and the launcher resolving a card's
1955
+ `repo@branch` to a path on this machine. `plugin/scripts/find-repos.sh` is a thin
1956
+ shim over it for shell callers, printing the git half only.
1957
+
1958
+ The **Hall** does not use it, and deliberately: choosing what an agent works on
1959
+ is a folder picker there. You walk your disk and take what you want — a
1960
+ repository, a worktree, a folder of documents, one exact file — and nothing is
1961
+ offered in advance or filtered out. A guessed list can only ever offer what it
1962
+ knew how to look for, and it is one more list to read before you can do the thing
1963
+ you came to do.
1825
1964
 
1826
1965
  The index is cached for one day at `$XDG_CACHE_HOME/agents-city/lugares.tsv` or
1827
1966
  `~/.cache/agents-city/lugares.tsv`. Both the Hall's house form and the seat offer
@@ -1908,10 +2047,18 @@ read your repos, attach to tmux, or read private files in your home. Use separat
1908
2047
  accounts, VMs, or containers for untrusted code, and also apply each provider
1909
2048
  CLI's permission controls.
1910
2049
 
1911
- A remote bus expands the trust surface. Deploy HTTPS/WSS, rotate tokens, limit
1912
- scopes, and read [docs/self-host.md](docs/self-host.md). A road authorises message
1913
- exchange between seats; it neither authorises execution of received commands nor
1914
- grants remote filesystem access.
2050
+ A remote bus expands the trust surface. For the self-hosted token transport,
2051
+ deploy HTTPS/WSS, rotate tokens, limit scopes, and read
2052
+ [docs/self-host.md](docs/self-host.md). Managed Connect instead uses device
2053
+ signatures, witnessed key transparency, hybrid X25519 + ML-KEM-768 session
2054
+ establishment, an Olm Double Ratchet and sealed delivery. Its private material
2055
+ is encrypted under an OS-keyring wrapping key in the cage-sealed
2056
+ `~/.agents-city/.runtime/connect/vault/` directory; see
2057
+ [docs/managed-connect.md](docs/managed-connect.md). A managed Road authorises
2058
+ encrypted reachability to the owner's human reception, not direct model input.
2059
+ Only the owner's later route makes the text available to selected cities. No
2060
+ Road authorises execution of received commands or grants remote filesystem
2061
+ access.
1915
2062
 
1916
2063
  ## Troubleshooting
1917
2064
 
@@ -2065,10 +2212,138 @@ agents-city seat --repos
2065
2212
  ```
2066
2213
 
2067
2214
  Discovery requires `.git` (a directory or worktree file) and an `origin` remote.
2068
- If roots just changed, use the house form's **Search again**, or run
2069
- `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` may be filtering the repo;
2215
+ If roots just changed, run `plugin/scripts/busca.py --refresh`. `AGENTS_CITY_ORG` may be filtering the repo;
2070
2216
  leave it empty to index every remote.
2071
2217
 
2218
+ ### Your CLIs, as you have them
2219
+
2220
+ This does not compete with the CLI you already run. It orchestrates them, which
2221
+ only works if it respects what you configured in them — your plugins, your
2222
+ skills, your MCP servers, your model, your permissions.
2223
+
2224
+ That is a claim about your machine, so it ships as a command rather than a
2225
+ promise:
2226
+
2227
+ ```bash
2228
+ agents-city doctor --config # what we add, inherit and leave alone
2229
+ agents-city doctor --config --json # the same, as data
2230
+ ```
2231
+
2232
+ It prints three columns per CLI, and the difference between them is the point:
2233
+
2234
+ * **the deal** — what we add or override. It is short, every line says *why*,
2235
+ and it is what makes the bus the only route between agents and the cage hold.
2236
+ Without it there is no product.
2237
+ * **we inherit** — what we deliberately do *not* send, so your own CLI reads
2238
+ your own configuration for it. Your model, your effort, your approval policy.
2239
+ * **untouched** — what loads exactly as it always did.
2240
+
2241
+ The report and the runtime read the **same file** — `plugin/channel/runtime/arnes.json`
2242
+ — so the claim cannot drift from the behaviour. The connectors take their policy
2243
+ values out of that declaration instead of spelling them inline, and the suite
2244
+ fails if a runtime imposes something the declaration does not mention. Writing
2245
+ that check found two: a system prompt injected into Kimi that nothing declared,
2246
+ and a sandbox value written in two places.
2247
+
2248
+ Where your setting and ours meet, yours wins where it can: Codex's
2249
+ `approval_policy` is honoured when you set one, and `on-request` is only the
2250
+ fallback when you have not. The report says the consequence out loud — `never`
2251
+ disables app and MCP tools — instead of quietly deciding you did not mean it.
2252
+
2253
+ ### Your chair keeps your own Claude Code
2254
+
2255
+ The seat window opens **Claude Code itself** — your plugins, your skills, your
2256
+ MCP servers, your statusline, slash-command completion, the model picker. It is
2257
+ the harness you already use, in the pane, and that is deliberate: the chair is
2258
+ where a person works by hand.
2259
+
2260
+ It is still on the bus. The city plugin's `SessionStart`, `UserPromptSubmit`,
2261
+ `Stop` and `SessionEnd` hooks report that session's prompts and answers as the
2262
+ same `conversation.*` events the gateway reports, so the town hall sees the
2263
+ conversation either way. And it carries the same two flags that make the bus the
2264
+ only route between agents — `crossSessionInbound: refuse` and
2265
+ `--disallowed-tools SendMessage,ListAgents`. A quieter product with a hole in it
2266
+ would not be a better product.
2267
+
2268
+ **Agent houses keep the gateway** and its `city>` prompt, because what the
2269
+ gateway buys is the bus being able to *push* work into a window — which is the
2270
+ whole job of a house and no part of the chair's.
2271
+
2272
+ One card key moves the chair back:
2273
+
2274
+ ```yaml
2275
+ ui.seat: gateway # the city's own prompt in the chair, as before
2276
+ ```
2277
+
2278
+ `CITY_UI=gateway` forces it for one launch. Houses are not asked: a house exists
2279
+ to receive assignments, and the gateway is what makes that possible.
2280
+
2281
+ ### The engine a house runs on
2282
+
2283
+ `model.<window>` and `effort.<window>` on the card say what a house runs on, once,
2284
+ whatever CLI runs it. Claude takes them as flags; the native gateways parse the
2285
+ same spelling out of the command string and send it with the turn — which is why
2286
+ one key means the same thing for all four:
2287
+
2288
+ | provider | model | effort |
2289
+ | --- | --- | --- |
2290
+ | `claude` | yes, an alias the CLI resolves (`opus`, `sonnet`…) | yes |
2291
+ | `codex` | yes, the name your Codex uses (`~/.codex/config.toml`) | yes |
2292
+ | `opencode` | yes, `provider/model` | no such setting |
2293
+ | `kimi` | yes | no such setting |
2294
+
2295
+ A command that already carries the flag keeps it: `runs.dbt: codex --model o3`
2296
+ was somebody saying what they meant, and a generic key must not overrule a
2297
+ specific sentence. Effort is written only where it is read, because a flag
2298
+ nothing reads is how a control ends up looking like it works.
2299
+
2300
+ ### Releasing
2301
+
2302
+ A release is a tag. Pushing `v0.5.2` runs the whole suite on Linux, macOS and
2303
+ Windows, checks that the tag and the three manifests name the same version, and
2304
+ publishes with **provenance** — a signed statement of which commit and which
2305
+ workflow produced that exact tarball. Anyone can check it:
2306
+
2307
+ ```bash
2308
+ npm audit signatures
2309
+ ```
2310
+
2311
+ No token is stored anywhere. It publishes through npm's trusted publishing,
2312
+ which trades a short-lived OIDC identity from the workflow for the right to
2313
+ publish this one package: a secret that does not exist cannot leak.
2314
+
2315
+ ```bash
2316
+ npm version patch --no-git-tag-version # then open a PR with the bump
2317
+ git tag v0.5.2 && git push origin v0.5.2 # the tag is the release
2318
+ ```
2319
+
2320
+ This exists because publishing by hand did not work. Four versions went
2321
+ unpublished in a single day, not because anybody was careless but because the
2322
+ step lived in a person's head and needed their passkey — and what reached the
2323
+ registry was whatever happened to be in a working directory, connected to no
2324
+ commit anyone could name.
2325
+
2326
+ ### Removing it completely
2327
+
2328
+ ```bash
2329
+ agents-city uninstall # says exactly what would go; removes nothing
2330
+ agents-city uninstall --yes # goes through with it
2331
+ agents-city uninstall --keep-cities --yes # unwire the machine, keep the cities
2332
+ agents-city uninstall --npm --yes # and remove the global package too
2333
+ ```
2334
+
2335
+ It closes every session, hall and map the product started, removes the desktop
2336
+ shortcuts and the Claude plugin registration, and deletes `~/.agents-city` (your
2337
+ cities, their state and their backups), `~/.config/agents-city`,
2338
+ `~/.cache/agents-city` and `~/.claude/channels/city-bus` — plus the bus token in
2339
+ the macOS Keychain.
2340
+
2341
+ It never touches your repositories, your worktrees or your document folders. An
2342
+ agent's home holds *links* to those, and a link is all that goes.
2343
+
2344
+ `reset` is the other question: it empties one city, keeps a backup and leaves the
2345
+ install in place, for when you mean to keep using it.
2346
+
2072
2347
  ### GitHub does not show private repos or organisations
2073
2348
 
2074
2349
  ```bash
@@ -72,6 +72,7 @@ function runNext() {
72
72
  working = true;
73
73
  const message = queue.shift();
74
74
  const text = userText(message);
75
+ const delay = behavior() === 'slow' ? 350 : 5;
75
76
  setTimeout(() => {
76
77
  emit({
77
78
  type: 'assistant',
@@ -95,7 +96,7 @@ function runNext() {
95
96
  working = false;
96
97
  runNext();
97
98
  }, 5);
98
- }, 5);
99
+ }, delay);
99
100
  }
100
101
 
101
102
  function userText(message) {
@@ -0,0 +1,16 @@
1
+ # Local reception ingest
2
+
3
+ This measures the durable, human-quarantine boundary only: decrypted messages
4
+ committed to private local SQLite in protocol-v4 batches. It uses no network,
5
+ relay, model, or fake model latency.
6
+
7
+ Build the public client, then run the 1,000-message/second gate:
8
+
9
+ ```bash
10
+ npm --prefix plugin/channel run build
11
+ node benchmarks/reception/run.mjs --messages 1000 --batch-size 32 --minimum 1000
12
+ ```
13
+
14
+ Passing this gate says the computer can quarantine the offered transport load
15
+ without loss. It does not say a human or model can answer 1,000 requests per
16
+ second. Pending-count and byte caps apply backpressure when review falls behind.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /** Reproducible local reception-ingest capacity check. No network or model. */
3
+
4
+ import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import {
9
+ receptionDatabasePath,
10
+ recordReceptionMessages,
11
+ } from '../../plugin/channel/managed-connect-client.js';
12
+
13
+ const integerArg = (name, fallback, minimum, maximum) => {
14
+ const index = process.argv.indexOf(name);
15
+ const value = index >= 0 ? Number(process.argv[index + 1]) : fallback;
16
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
17
+ throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`);
18
+ }
19
+ return value;
20
+ };
21
+
22
+ const total = integerArg('--messages', 1_000, 1, 100_000);
23
+ const batchSize = integerArg('--batch-size', 32, 1, 32);
24
+ const minimum = integerArg('--minimum', 1_000, 1, 1_000_000);
25
+ const root = mkdtempSync(join(tmpdir(), 'agents-city-reception-bench-'));
26
+ const context = {
27
+ dataDir: root,
28
+ appHome: root,
29
+ runtimeDir: join(root, '.runtime', 'bus', 'city-target'),
30
+ owner: 'owner',
31
+ city: { id: 'city_target', address: 'owner/target', name: 'Target' },
32
+ domain: 'custom',
33
+ seatRole: '',
34
+ actors: { seat: { role: 'chair' } },
35
+ engines: { seat: 'claude' },
36
+ roads: [],
37
+ };
38
+ const envelope = (index) => ({
39
+ protocol: 'agents-city-bus/2',
40
+ id: `managed_bench_${String(index).padStart(8, '0')}`,
41
+ kind: 'road.message',
42
+ scope: 'road',
43
+ thread: null,
44
+ from: { city: 'peer/source', actor: 'seat', role: 'external-seat' },
45
+ to: { city: context.city.address, actor: 'seat' },
46
+ createdAt: '2026-08-28T12:00:00.000Z',
47
+ payload: {
48
+ text: 'x'.repeat(256),
49
+ transport: 'managed-e2ee',
50
+ roadId: 'road_benchmark',
51
+ remoteMessageId: String(index),
52
+ },
53
+ });
54
+
55
+ try {
56
+ // Exclude schema creation and first WAL setup from steady ingest.
57
+ recordReceptionMessages(context, [envelope(total)]);
58
+ const messages = Array.from({ length: total }, (_, index) => envelope(index));
59
+ const started = performance.now();
60
+ for (let index = 0; index < messages.length; index += batchSize) {
61
+ recordReceptionMessages(context, messages.slice(index, index + batchSize));
62
+ }
63
+ const seconds = (performance.now() - started) / 1_000;
64
+ const database = new DatabaseSync(receptionDatabasePath(root), { readOnly: true });
65
+ const persisted = Number(
66
+ database.prepare('SELECT COUNT(*) AS count FROM reception_messages').get().count,
67
+ ) - 1;
68
+ database.close();
69
+ const messagesPerSecond = total / seconds;
70
+ const result = {
71
+ messages: total,
72
+ batchSize,
73
+ persisted,
74
+ lost: total - persisted,
75
+ seconds: Number(seconds.toFixed(3)),
76
+ messagesPerSecond: Number(messagesPerSecond.toFixed(1)),
77
+ minimum,
78
+ passed: persisted === total && messagesPerSecond >= minimum,
79
+ };
80
+ console.log(JSON.stringify(result, null, 2));
81
+ if (!result.passed) process.exitCode = 1;
82
+ } finally {
83
+ rmSync(root, { recursive: true, force: true });
84
+ }
@@ -25,11 +25,16 @@ const ORDENES = {
25
25
  cities: { que: ['bin/cities'], di: 'list, create or select your cities' },
26
26
  agents: { que: ['bin/agents'], di: 'list agents and manage their workspace mounts' },
27
27
  road: { que: ['bin/road'], di: 'connect two cities explicitly' },
28
+ connect: { que: ['bin/connect'], di: 'pair this computer and open encrypted managed Roads' },
28
29
  bus: { que: ['bin/bus'], di: 'send information over those roads (seat only)' },
29
30
  committee: { que: ['bin/committee'], di: 'chair-mediated work with repo agents' },
30
31
  benchmark: { que: ['bin/benchmark'], di: 'offline stress/governance or opt-in live runtime latency' },
31
32
  logs: { que: ['bin/logs'], di: 'read or follow visible activity and operational diagnostics' },
32
33
  reset: { que: ['bin/reset'], di: 'reset one city to onboarding, recoverably' },
34
+ uninstall: {
35
+ que: ['bin/uninstall'],
36
+ di: 'remove everything this wrote on this machine (previews first)',
37
+ },
33
38
  skills: { que: ['bin/skills'], di: 'recognise the skills installed in each repo' },
34
39
  city: { que: ['bin/city'], di: 'draw your city' },
35
40
  shortcut: { que: ['bin/shortcut'], di: 'put a city on your desktop: icon, name, double-click' },
package/bin/connect ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Pair this computer and connect one or more local cities to managed Roads.
3
+ set -euo pipefail
4
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
+ exec node "$ROOT/plugin/channel/managed-connect-cli.js" "$@"