city-cli 0.9.1__py3-none-any.whl

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 (66) hide show
  1. city/__init__.py +1 -0
  2. city/testkit/__init__.py +5 -0
  3. city/testkit/keys.py +9 -0
  4. city/testkit/py.typed +0 -0
  5. city_cli-0.9.1.dist-info/METADATA +111 -0
  6. city_cli-0.9.1.dist-info/RECORD +66 -0
  7. city_cli-0.9.1.dist-info/WHEEL +4 -0
  8. city_cli-0.9.1.dist-info/entry_points.txt +2 -0
  9. city_conform/__init__.py +3 -0
  10. city_conform/__main__.py +21 -0
  11. city_conform/agent.py +283 -0
  12. city_conform/minimarket.py +66 -0
  13. city_conform/remote.py +559 -0
  14. city_conform/report.py +50 -0
  15. city_conform/runner.py +122 -0
  16. city_conform/stages.py +457 -0
  17. city_conform/world.py +117 -0
  18. city_deploy/__init__.py +3 -0
  19. city_deploy/__main__.py +6 -0
  20. city_deploy/api.py +282 -0
  21. city_deploy/cli.py +554 -0
  22. city_deploy/clock.py +28 -0
  23. city_deploy/conform.py +105 -0
  24. city_deploy/errors.py +29 -0
  25. city_deploy/local.py +89 -0
  26. city_deploy/quota.py +48 -0
  27. city_deploy/receipt.py +34 -0
  28. city_deploy/runtime.py +724 -0
  29. city_deploy/secrets.py +125 -0
  30. city_deploy/serve.py +45 -0
  31. city_deploy/service.py +943 -0
  32. city_deploy/skeleton.py +298 -0
  33. city_deploy/state.py +10 -0
  34. city_deploy/templates/b2b/Dockerfile +9 -0
  35. city_deploy/templates/b2b/SKILL.md +3 -0
  36. city_deploy/templates/b2b/brain/brain.py +8 -0
  37. city_deploy/templates/b2b/card.yaml +10 -0
  38. city_deploy/templates/b2b/city.yaml +7 -0
  39. city_deploy/templates/cup/Dockerfile +9 -0
  40. city_deploy/templates/cup/SKILL.md +5 -0
  41. city_deploy/templates/cup/brain/brain.py +11 -0
  42. city_deploy/templates/cup/card.yaml +14 -0
  43. city_deploy/templates/cup/city.yaml +7 -0
  44. city_deploy/templates/facility/Dockerfile +9 -0
  45. city_deploy/templates/facility/SKILL.md +3 -0
  46. city_deploy/templates/facility/brain/brain.py +7 -0
  47. city_deploy/templates/facility/card.yaml +16 -0
  48. city_deploy/templates/facility/city.yaml +7 -0
  49. city_deploy/templates/mmg/Dockerfile +9 -0
  50. city_deploy/templates/mmg/SKILL.md +3 -0
  51. city_deploy/templates/mmg/brain/brain.py +6 -0
  52. city_deploy/templates/mmg/card.yaml +10 -0
  53. city_deploy/templates/mmg/city.yaml +7 -0
  54. city_index/__init__.py +3 -0
  55. city_index/__main__.py +24 -0
  56. city_index/app.py +109 -0
  57. city_index/auth.py +15 -0
  58. city_index/cache.py +43 -0
  59. city_index/catalog.py +233 -0
  60. city_index/events.py +38 -0
  61. city_index/model.py +43 -0
  62. city_index/ports.py +15 -0
  63. city_index/query.py +91 -0
  64. city_index/service.py +201 -0
  65. city_index/visibility.py +52 -0
  66. city_index/watch.py +35 -0
city/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """City libraries (``city.testkit``, later reputation and others)."""
@@ -0,0 +1,5 @@
1
+ """Test helpers. Ephemeral keys only; nothing here is a credential store."""
2
+
3
+ from city.testkit.keys import ephemeral
4
+
5
+ __all__ = ["ephemeral"]
city/testkit/keys.py ADDED
@@ -0,0 +1,9 @@
1
+ """Generate an Ed25519 key pair in memory. The private key is not serialized."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cap.core.keys import KeyPair, generate
6
+
7
+
8
+ def ephemeral() -> KeyPair:
9
+ return generate()
city/testkit/py.typed ADDED
File without changes
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: city-cli
3
+ Version: 0.9.1
4
+ Summary: Milano Agentic City CLI (city): init, dev, conform, and deploy.
5
+ Project-URL: Homepage, https://github.com/projectnandaitaly/agentic-city
6
+ Project-URL: Repository, https://github.com/projectnandaitaly/agentic-city
7
+ Project-URL: Documentation, https://github.com/projectnandaitaly/agentic-city/blob/main/services/deploy/README.md
8
+ Project-URL: Issues, https://github.com/projectnandaitaly/agentic-city/issues
9
+ Author: Project NANDA Italy
10
+ License: Proprietary
11
+ Keywords: agentic-city,cli,deploy,nanda
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: city-sdk>=0.9.1
21
+ Requires-Dist: cryptography>=43
22
+ Requires-Dist: fastapi>=0.115
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: pydantic>=2
25
+ Requires-Dist: rfc8785>=0.1.4
26
+ Requires-Dist: typer>=0.12
27
+ Requires-Dist: uvicorn>=0.32
28
+ Description-Content-Type: text/markdown
29
+
30
+ # city-cli
31
+
32
+ Command-line tool for **Milano Agentic City** teams.
33
+
34
+ Install once, then scaffold an agent, run it locally, and publish it through the City Deploy API — **without GCP credentials** (ADR-010).
35
+
36
+ ## Requirements
37
+
38
+ - Python **3.12+**
39
+ - For live deploy: Deploy API URL (see below)
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pipx install city-cli
45
+ # or
46
+ uv tool install city-cli
47
+ # or
48
+ pip install city-cli
49
+ ```
50
+
51
+ This provides the `city` command. It depends on [`city-sdk`](https://pypi.org/project/city-sdk/) for the Market facade and harness types.
52
+
53
+ ## Live city (remote Deploy API)
54
+
55
+ ```bash
56
+ export CITY_DEPLOY_URL=https://deploy.projectnandaitaly.ai
57
+
58
+ city login --team t07
59
+ city keys init
60
+ city init --role cup --name demo
61
+ city card validate
62
+ city deploy --promote
63
+ city status agent:t07/demo
64
+ ```
65
+
66
+ Per-command override:
67
+
68
+ ```bash
69
+ city --api-url https://deploy.projectnandaitaly.ai status agent:t07/demo
70
+ ```
71
+
72
+ JSON for scripting:
73
+
74
+ ```bash
75
+ city --json --api-url "$CITY_DEPLOY_URL" status agent:t07/demo
76
+ ```
77
+
78
+ ## Local kit (no API URL)
79
+
80
+ If `CITY_DEPLOY_URL` / `--api-url` is unset, `city` uses an **in-process** Deploy service (useful for `init`, `dev`, and offline checks).
81
+
82
+ ```bash
83
+ city init --role cup --name demo
84
+ city dev
85
+ city conform --profile fast
86
+ ```
87
+
88
+ ## Commands (overview)
89
+
90
+ | Command | Purpose |
91
+ |---|---|
92
+ | `city login` | Device-flow team session; stores token under `.city/` |
93
+ | `city keys init` | Local controller key + register public key |
94
+ | `city init` | Scaffold `cup` / `facility` / `mmg` / `b2b` template |
95
+ | `city card validate` | Basic card.yaml checks |
96
+ | `city dev` | One local need through the harness |
97
+ | `city conform` | Conformance suite (`--profile full\|fast`, or `--url`) |
98
+ | `city deploy` | Upload + pipeline (`--promote`, `--runtime`) |
99
+ | `city promote` / `rollback` / `status` / `retire` | Lifecycle |
100
+ | `city secrets set` / `city scale` / `city llm key rotate` | Ops |
101
+ | `city compliance` / `city verify` | Receipts and offline verify |
102
+
103
+ ## Links
104
+
105
+ - Source: [projectnandaitaly/agentic-city](https://github.com/projectnandaitaly/agentic-city)
106
+ - SDK: [city-sdk on PyPI](https://pypi.org/project/city-sdk/)
107
+ - Spec: City Deploy CLI §10.2 in the repo docs
108
+
109
+ ## License
110
+
111
+ Proprietary — Project NANDA Italy / Milano Agentic City event kit.
@@ -0,0 +1,66 @@
1
+ city/__init__.py,sha256=g4CSy6E8XGT896Y4HxXH_eSSfRhlnhHQfUTMwlx5aqs,70
2
+ city/testkit/__init__.py,sha256=8MI3b5ROYsn6vytI-BuFZkalmnTiyObSvWNsvGcNeNs,143
3
+ city/testkit/keys.py,sha256=5CpOkcqFGMOzCBiFhNGtyF08709ABFjX3GJU0Tw6Kn0,214
4
+ city/testkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ city_conform/__init__.py,sha256=RIEeuTsAgRLS2C2KID2A8nupbX5s353VXR0BEd4vkq8,83
6
+ city_conform/__main__.py,sha256=Vrdtq7KhpVVs3XtdZ7faom4jTpjhTXXDRtn5e_lKqk0,561
7
+ city_conform/agent.py,sha256=4JLjz3rCb6_lIQutJxkuhUP18up1IbLgMiEFWSqZW0o,8123
8
+ city_conform/minimarket.py,sha256=jkmjuelg496sOnU0L_1dgilzbPmmsdB5jg8Pj6FhFmo,1996
9
+ city_conform/remote.py,sha256=Y-rxCs_TfJmWa7risdJvk-1ZOrLP98pxxD4_RsCScrQ,17589
10
+ city_conform/report.py,sha256=nlHOwb_G3TJca4Zn_Kg2qQ9vpFvkVdjv9Kk_zMl6Ad4,1596
11
+ city_conform/runner.py,sha256=eZN6-8YmPJ1ji3BVFwowYaAV6k4bCbjJ8fh1hcJFlQM,2951
12
+ city_conform/stages.py,sha256=zvzRJ4Fqi7pUsyWCMDmdlKHDnABNg2veGfaK_ByYAXE,16952
13
+ city_conform/world.py,sha256=Tp7ZVJ2PynomtHI4LoNuuoy9hRVCaiSxpKJZXGh31DE,3528
14
+ city_deploy/__init__.py,sha256=jhAKlfccuKvMIDi32IVu_ltD1lfj4ti8gpoOcyGNNec,106
15
+ city_deploy/__main__.py,sha256=klSOFFSHoLxBN8c4dUWF5TkcRWeKu5z0tAWwUCqLxFE,124
16
+ city_deploy/api.py,sha256=hLH9OLWct9_sSDQI3sAwZPmhXPAscj2mc-J_0bid6QU,10625
17
+ city_deploy/cli.py,sha256=zlkTUeK3rjPeLu2azpAy4HQOjXJcJgz246lGWGiDPTY,18198
18
+ city_deploy/clock.py,sha256=MyxKmvAdiOC9jifUTF_Ww825yuVGEl0rAxzDCjeLruo,735
19
+ city_deploy/conform.py,sha256=9MOykTBmi0u3xqRTf7IsqfkMDfn-2JnAyvyJ-dQ65LA,3445
20
+ city_deploy/errors.py,sha256=IBxRpT7SYBfkEnZYE5O0dZyXpuLlWSs1yCue1t-st_Q,827
21
+ city_deploy/local.py,sha256=RGZLZ2bC5kiVQNgtKyXd9xaAXIJWSfAZ2ByJD55luhY,3237
22
+ city_deploy/quota.py,sha256=MAg9kk8GcKjMFdtLaMzJLdxFjwH-QruixfvA9RMwEP4,1418
23
+ city_deploy/receipt.py,sha256=5X8ow5fXaF5K_OmYEKf7U5iaeW3p1tGhNz_rOPuLIss,878
24
+ city_deploy/runtime.py,sha256=r-L-bIuyygyhNOS7ljpfg2ejzB9Mq71GEhBxyDxjdvE,25296
25
+ city_deploy/secrets.py,sha256=xrf5ihWA46dNhaxMVG6wB633ZRuIpeUu1r6joMgO9pM,4336
26
+ city_deploy/serve.py,sha256=pUjAZqsxct_ECKjXDf6jbaxd8MY5TqXlfj3yisXuhBE,1394
27
+ city_deploy/service.py,sha256=cKfId69n377bOOBq9BxfPVfcYjPVLStmjdYTP1n1oO8,34484
28
+ city_deploy/skeleton.py,sha256=z2bx_t-PBL4__IEa6qafmeZpd3GoHI5f84oclcxz9x0,9941
29
+ city_deploy/state.py,sha256=XaDx4WdcDl2GigkGtMTUPvTDtT_op8Gu4Y5X9XOMdIs,276
30
+ city_deploy/templates/b2b/Dockerfile,sha256=JgarZrUCv7N5IgsX7KZzH087Q7raPDUMrz9Vbn60qF8,226
31
+ city_deploy/templates/b2b/SKILL.md,sha256=iS8kpITfRqqg9_bopFGY_kV1TvbWgaNOaD0kSNmbnKg,99
32
+ city_deploy/templates/b2b/card.yaml,sha256=vokpQ4W8ZDV2RX7gTFCZfl259Mv3mfeh3L0lS0NfgVI,156
33
+ city_deploy/templates/b2b/city.yaml,sha256=lvyrzSuhaNtdNxfSWW9-NIPSJFwG7HB8f_ii92eDxAc,124
34
+ city_deploy/templates/b2b/brain/brain.py,sha256=XYb9Z2uTG8c6BA-_lea94fTFkPmut_pnsk6rFBImyUE,276
35
+ city_deploy/templates/cup/Dockerfile,sha256=JgarZrUCv7N5IgsX7KZzH087Q7raPDUMrz9Vbn60qF8,226
36
+ city_deploy/templates/cup/SKILL.md,sha256=DRpTkBZCkr1gsbBvzoH71vV2chZ56ddObzmkRyVgHdE,171
37
+ city_deploy/templates/cup/card.yaml,sha256=41k7TrhQuex3E6UxVfPT_EKwBYMqOhA0v9hXJTl9-GY,218
38
+ city_deploy/templates/cup/city.yaml,sha256=IPDhXNMUUTBiGyxDMJpC44IHZxGDInnp68_OH380k_w,126
39
+ city_deploy/templates/cup/brain/brain.py,sha256=SjEm4uPVnLru3xBpkm6wHXz9O_UFAF3EgQU5DHIcCBs,396
40
+ city_deploy/templates/facility/Dockerfile,sha256=JgarZrUCv7N5IgsX7KZzH087Q7raPDUMrz9Vbn60qF8,226
41
+ city_deploy/templates/facility/SKILL.md,sha256=gXDdHeOHGlDtxH73ZaZzghp2SEK7YiJzb3BIWTbdboY,80
42
+ city_deploy/templates/facility/card.yaml,sha256=BGtLhCkLSkvob3TjwWH6FWvjZ8jYgIgVCxp8vH_jq7Q,234
43
+ city_deploy/templates/facility/city.yaml,sha256=tRmOKeTAsJEtSCr5-SSl5UfTOi7nFJmEmjcfE_0BX14,131
44
+ city_deploy/templates/facility/brain/brain.py,sha256=iA4lA8rOh6SX8UXyhAXPx1e70Rp6HIK3mwTPd2c-49I,239
45
+ city_deploy/templates/mmg/Dockerfile,sha256=JgarZrUCv7N5IgsX7KZzH087Q7raPDUMrz9Vbn60qF8,226
46
+ city_deploy/templates/mmg/SKILL.md,sha256=eWS_y2zDiD_UPLJuYdXiRg8wvE_XMTBHfvGtTIuMcx4,81
47
+ city_deploy/templates/mmg/card.yaml,sha256=5mqdSzx9OFF57jXBxywzejin7C2A3_QXAw_E0G0Jj6A,154
48
+ city_deploy/templates/mmg/city.yaml,sha256=X4uJpKR1NKhMri2OO7s_h5_8-uWKXTKS28F1aaUR5jg,123
49
+ city_deploy/templates/mmg/brain/brain.py,sha256=Chy96m5Lk0o49vEdeYImjleoX3A-256g2-b137XjDAM,180
50
+ city_index/__init__.py,sha256=668tBJkzls0ek-rQYyZoPOWSeYbsFX-7kDdQ2WR37jA,107
51
+ city_index/__main__.py,sha256=Jtrr5y_gqSlK9R0okEjXs-54GNraYYdeVot8fz8kU7Y,518
52
+ city_index/app.py,sha256=pEebnNgDkUt8DaNzr77Xw-4ueQJ1x3adkHe7Qp9Hdww,3831
53
+ city_index/auth.py,sha256=cQ-FN0V42zWd3sa9jVjU7wavWQP5mDnKuok8b1fLzz8,391
54
+ city_index/cache.py,sha256=p_A5oLltIvSh6Icbeza-tEWM2-7PfAh0exNXwVZ1ng0,1292
55
+ city_index/catalog.py,sha256=4-eshhluAcoRGXX7QsS21CWh4qMUkk1xG9cpLAwTY_M,8100
56
+ city_index/events.py,sha256=Ax2jw8FH0Z1N3XiK8gPtipIfXJMRot4U2hLuTsbZ0AY,837
57
+ city_index/model.py,sha256=8tj7UX1ndpp1-5CuED_m-wGxDsFQMwZGFDk1rstNa1o,1077
58
+ city_index/ports.py,sha256=xeofc3rdAJTAfQqSpkpoNKiwf-6h_IyneR2R4BCOYRM,340
59
+ city_index/query.py,sha256=A6NMi2ARXGBRQCfHn5bVlcbClMIkZj40bBoen_yfXt8,2376
60
+ city_index/service.py,sha256=55KWF7_CqCVoXRkLR10hmFsDC41JzIxcFrg8bih2fvw,6490
61
+ city_index/visibility.py,sha256=yEvZErKoV72uDwevNeWEL4hriZiFiz5f4Ur-XMWqJhg,1488
62
+ city_index/watch.py,sha256=WcO25BJvPVsQolDM4G9anXuBJlMmI2vpLd9_Q8kXy_w,1139
63
+ city_cli-0.9.1.dist-info/METADATA,sha256=bdiGCNV16BpDSUna9Tobwi0PdhdIN6UGdJC3PnZsAaM,3396
64
+ city_cli-0.9.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
65
+ city_cli-0.9.1.dist-info/entry_points.txt,sha256=JBXcJL0plncYiVf01spWaVjnNDbE1aiyd71xHaTj0yw,46
66
+ city_cli-0.9.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ city = city_deploy.cli:main
@@ -0,0 +1,3 @@
1
+ """Conformance runner. Outcomes follow specification 09."""
2
+
3
+ __version__ = "0.9.0"
@@ -0,0 +1,21 @@
1
+ """`city-conform` runs the in-process full profile and prints the report."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from city_conform.report import render
8
+ from city_conform.runner import run_profile
9
+ from city_conform.stages import CATALOG
10
+ from city_conform.world import good_agent
11
+
12
+
13
+ def main() -> None:
14
+ async def _run() -> bool:
15
+ agent = await good_agent()
16
+ report = await run_profile(agent, CATALOG)
17
+ print(render(report), end="")
18
+ return report.passed
19
+
20
+ if not asyncio.run(_run()):
21
+ raise SystemExit(1)
city_conform/agent.py ADDED
@@ -0,0 +1,283 @@
1
+ """Agent under test. The live agent is the harness behind ASGI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timedelta
8
+ from typing import Any, Protocol, cast
9
+
10
+ from cap.core.envelope import build, sign
11
+ from cap.core.keys import PrivateKey, PublicKey
12
+ from cap.core.timeutil import format_time
13
+ from city_sdk.app import create_app
14
+ from city_sdk.harness import Harness
15
+ from httpx import ASGITransport, AsyncClient
16
+
17
+ Sender = tuple[str, PrivateKey]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SuiteSigners:
22
+ """City and peer keys the agent already trusts. The runner does not invent them."""
23
+
24
+ sender: Sender
25
+ prober: Sender
26
+ city: Sender
27
+ stranger: Sender
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class HttpResult:
32
+ status: int
33
+ body: dict[str, Any]
34
+
35
+
36
+ class Agent(Protocol):
37
+ def now(self) -> datetime: ...
38
+
39
+ def public_key(self) -> PublicKey: ...
40
+
41
+ def effect_count(self) -> int: ...
42
+
43
+ def ack_log(self) -> list[tuple[str, int, str]]: ...
44
+
45
+ def renew_log(self) -> list[tuple[str, int]]: ...
46
+
47
+ def sent_log(self) -> list[tuple[str, dict[str, object], str | None]]: ...
48
+
49
+ def hold_log(self) -> list[tuple[str, str]]: ...
50
+
51
+ def memory_view(self) -> dict[str, str]: ...
52
+
53
+ def escalation(self) -> str | None: ...
54
+
55
+ async def stop(self) -> None: ...
56
+
57
+ async def request(
58
+ self, method: str, path: str, payload: dict[str, Any] | None = None
59
+ ) -> HttpResult: ...
60
+
61
+ def need(
62
+ self,
63
+ *,
64
+ body: dict[str, Any],
65
+ msg_id: str | None = None,
66
+ fence: int = 1,
67
+ expires_at: datetime | None = None,
68
+ ) -> dict[str, Any]: ...
69
+
70
+ def unsigned_need(self) -> dict[str, Any]: ...
71
+
72
+ def bad_signature_need(self) -> dict[str, Any]: ...
73
+
74
+ def unknown_key_need(self) -> dict[str, Any]: ...
75
+
76
+ def expired_need(self) -> dict[str, Any]: ...
77
+
78
+ def probe(self, nonce: str) -> dict[str, Any]: ...
79
+
80
+ def order(self, *, epoch: int, kind: str, sender: Sender | None = None) -> dict[str, Any]: ...
81
+
82
+ def foreign_order(self) -> dict[str, Any]: ...
83
+
84
+
85
+ def _tamper(token: str) -> str:
86
+ first = token[0]
87
+ replacement = "A" if first != "A" else "B"
88
+ return replacement + token[1:]
89
+
90
+
91
+ class LiveAgent:
92
+ def __init__(
93
+ self,
94
+ harness: Harness,
95
+ *,
96
+ sender: Sender,
97
+ prober: Sender,
98
+ city: Sender,
99
+ stranger: Sender,
100
+ ) -> None:
101
+ self.harness = harness
102
+ self._sender = sender
103
+ self._prober = prober
104
+ self._city = city
105
+ self._stranger = stranger
106
+ self._app = create_app(harness)
107
+
108
+ def now(self) -> datetime:
109
+ return self.harness.clock.now()
110
+
111
+ def public_key(self) -> PublicKey:
112
+ return self.harness.public
113
+
114
+ def effect_count(self) -> int:
115
+ return self.harness.brain_calls
116
+
117
+ def ack_log(self) -> list[tuple[str, int, str]]:
118
+ return list(self.harness.relay.acks)
119
+
120
+ def renew_log(self) -> list[tuple[str, int]]:
121
+ return list(self.harness.relay.renewed)
122
+
123
+ def sent_log(self) -> list[tuple[str, dict[str, object], str | None]]:
124
+ return list(self.harness.relay.sent)
125
+
126
+ def hold_log(self) -> list[tuple[str, str]]:
127
+ return list(self.harness.ledger.calls)
128
+
129
+ def memory_view(self) -> dict[str, str]:
130
+ return dict(self.harness.memory)
131
+
132
+ def escalation(self) -> str | None:
133
+ market = self.harness.last_market
134
+ if market is None:
135
+ return None
136
+ return market.escalated
137
+
138
+ async def stop(self) -> None:
139
+ await self.harness.shutdown()
140
+
141
+ async def request(
142
+ self, method: str, path: str, payload: dict[str, Any] | None = None
143
+ ) -> HttpResult:
144
+ async with AsyncClient(
145
+ transport=ASGITransport(app=self._app),
146
+ base_url="http://agent",
147
+ ) as client:
148
+ if method == "GET":
149
+ response = await client.get(path)
150
+ else:
151
+ response = await client.post(path, json=payload)
152
+ body: dict[str, Any] = {}
153
+ try:
154
+ parsed = response.json()
155
+ except ValueError:
156
+ parsed = None
157
+ if isinstance(parsed, dict):
158
+ body = parsed
159
+ return HttpResult(response.status_code, body)
160
+
161
+ def need(
162
+ self,
163
+ *,
164
+ body: dict[str, Any],
165
+ msg_id: str | None = None,
166
+ fence: int = 1,
167
+ expires_at: datetime | None = None,
168
+ ) -> dict[str, Any]:
169
+ return self._delivery(
170
+ self._sender,
171
+ "need.request",
172
+ body,
173
+ msg_id=msg_id,
174
+ fence=fence,
175
+ expires_at=expires_at,
176
+ )
177
+
178
+ def unsigned_need(self) -> dict[str, Any]:
179
+ msg_id = str(uuid.uuid4())
180
+ envelope = build(
181
+ msg_id=msg_id,
182
+ message_type="need.request",
183
+ sender=self._sender[0],
184
+ recipient=self.harness.agent_id,
185
+ sent_at=self.now(),
186
+ body={"service": "lab", "summary": "visit"},
187
+ )
188
+ return self._wrap(msg_id, envelope, fence=1)
189
+
190
+ def bad_signature_need(self) -> dict[str, Any]:
191
+ payload = self.need(body={"service": "lab", "summary": "visit"})
192
+ envelope = payload["envelope"]
193
+ sig = dict(envelope["sig"])
194
+ sig["jws"] = _tamper(str(sig["jws"]))
195
+ envelope["sig"] = sig
196
+ return payload
197
+
198
+ def unknown_key_need(self) -> dict[str, Any]:
199
+ return self._delivery(
200
+ self._stranger,
201
+ "need.request",
202
+ {"service": "lab", "summary": "visit"},
203
+ )
204
+
205
+ def expired_need(self) -> dict[str, Any]:
206
+ return self.need(
207
+ body={"service": "lab", "summary": "visit"},
208
+ expires_at=self.now() - timedelta(seconds=5),
209
+ )
210
+
211
+ def probe(self, nonce: str) -> dict[str, Any]:
212
+ return self._delivery(self._prober, "cap.probe", {"nonce": nonce})
213
+
214
+ def order(self, *, epoch: int, kind: str, sender: Sender | None = None) -> dict[str, Any]:
215
+ who = self._city if sender is None else sender
216
+ msg_id = str(uuid.uuid4())
217
+ envelope = sign(
218
+ build(
219
+ msg_id=msg_id,
220
+ message_type="city.order",
221
+ sender=who[0],
222
+ recipient=self.harness.agent_id,
223
+ sent_at=self.now(),
224
+ body={
225
+ "order_id": f"ord:{epoch}:001",
226
+ "epoch": epoch,
227
+ "kind": kind,
228
+ "requires_ack": True,
229
+ },
230
+ ),
231
+ who[1],
232
+ kid=who[0] + "#k1",
233
+ )
234
+ return cast(dict[str, Any], envelope)
235
+
236
+ def foreign_order(self) -> dict[str, Any]:
237
+ return self.order(epoch=1, kind="ping", sender=self._sender)
238
+
239
+ def suite_signers(self) -> SuiteSigners:
240
+ return SuiteSigners(
241
+ sender=self._sender,
242
+ prober=self._prober,
243
+ city=self._city,
244
+ stranger=self._stranger,
245
+ )
246
+
247
+ def _delivery(
248
+ self,
249
+ sender: Sender,
250
+ message_type: str,
251
+ body: dict[str, Any],
252
+ *,
253
+ msg_id: str | None = None,
254
+ fence: int = 1,
255
+ expires_at: datetime | None = None,
256
+ ) -> dict[str, Any]:
257
+ chosen = msg_id if msg_id is not None else str(uuid.uuid4())
258
+ envelope = sign(
259
+ build(
260
+ msg_id=chosen,
261
+ message_type=message_type,
262
+ sender=sender[0],
263
+ recipient=self.harness.agent_id,
264
+ sent_at=self.now(),
265
+ body=body,
266
+ expires_at=expires_at,
267
+ ),
268
+ sender[1],
269
+ kid=sender[0] + "#k1",
270
+ )
271
+ return self._wrap(chosen, envelope, fence=fence)
272
+
273
+ def _wrap(self, msg_id: str, envelope: dict[str, Any], *, fence: int) -> dict[str, Any]:
274
+ return {
275
+ "delivery": {
276
+ "msg_id": msg_id,
277
+ "mailbox": "m-1",
278
+ "fence": fence,
279
+ "attempt": 1,
280
+ "lease_until": format_time(self.now()),
281
+ },
282
+ "envelope": envelope,
283
+ }
@@ -0,0 +1,66 @@
1
+ """Scripted role checks. The runner does not reorder Index candidates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from city_conform.agent import Agent
6
+ from city_conform.runner import Verdict, failed, passed
7
+ from city_conform.world import FIRST
8
+
9
+
10
+ async def cup(agent: Agent) -> Verdict:
11
+ sent_before = len(agent.sent_log())
12
+ await agent.request(
13
+ "POST",
14
+ "/invoke",
15
+ agent.need(
16
+ body={"service": "lab", "summary": "alarm", "is_emergency": True, "municipio": 8}
17
+ ),
18
+ )
19
+ if agent.escalation() != "red_flag" or len(agent.sent_log()) != sent_before:
20
+ return failed("cup emergency")
21
+ await agent.request(
22
+ "POST",
23
+ "/invoke",
24
+ agent.need(body={"service": "lab", "summary": "visit", "municipio": 8}),
25
+ )
26
+ sent = agent.sent_log()
27
+ if len(sent) != sent_before + 1 or sent[-1][0] != FIRST.agent_id:
28
+ return failed("cup ask")
29
+ return passed()
30
+
31
+
32
+ async def facility(agent: Agent) -> Verdict:
33
+ before = len(agent.hold_log())
34
+ await agent.request(
35
+ "POST",
36
+ "/invoke",
37
+ agent.need(body={"service": "lab", "summary": "slot", "municipio": 8}),
38
+ )
39
+ holds = agent.hold_log()
40
+ if len(holds) != before + 1 or holds[-1] != ("hold", FIRST.agent_id):
41
+ return failed("facility hold")
42
+ return passed()
43
+
44
+
45
+ async def mmg(agent: Agent) -> Verdict:
46
+ await agent.request(
47
+ "POST",
48
+ "/invoke",
49
+ agent.need(body={"service": "lab", "summary": "follow-up", "municipio": 8}),
50
+ )
51
+ if agent.memory_view().get("last") != "follow-up":
52
+ return failed("mmg memory")
53
+ return passed()
54
+
55
+
56
+ async def b2b(agent: Agent) -> Verdict:
57
+ before = len(agent.sent_log())
58
+ await agent.request(
59
+ "POST",
60
+ "/invoke",
61
+ agent.need(body={"service": "lab", "summary": "desk", "municipio": 8}),
62
+ )
63
+ sent = agent.sent_log()
64
+ if len(sent) != before + 1 or sent[-1][0] != FIRST.agent_id:
65
+ return failed("b2b send")
66
+ return passed()