city-cli 0.9.1__tar.gz
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.
- city_cli-0.9.1/.gitignore +73 -0
- city_cli-0.9.1/PKG-INFO +111 -0
- city_cli-0.9.1/README.md +82 -0
- city_cli-0.9.1/VERSION +1 -0
- city_cli-0.9.1/pyproject.toml +44 -0
- city_cli-0.9.1/src/city/__init__.py +1 -0
- city_cli-0.9.1/src/city/testkit/__init__.py +5 -0
- city_cli-0.9.1/src/city/testkit/keys.py +9 -0
- city_cli-0.9.1/src/city/testkit/py.typed +0 -0
- city_cli-0.9.1/src/city_conform/__init__.py +3 -0
- city_cli-0.9.1/src/city_conform/__main__.py +21 -0
- city_cli-0.9.1/src/city_conform/agent.py +283 -0
- city_cli-0.9.1/src/city_conform/minimarket.py +66 -0
- city_cli-0.9.1/src/city_conform/remote.py +559 -0
- city_cli-0.9.1/src/city_conform/report.py +50 -0
- city_cli-0.9.1/src/city_conform/runner.py +122 -0
- city_cli-0.9.1/src/city_conform/stages.py +457 -0
- city_cli-0.9.1/src/city_conform/world.py +117 -0
- city_cli-0.9.1/src/city_deploy/__init__.py +3 -0
- city_cli-0.9.1/src/city_deploy/__main__.py +6 -0
- city_cli-0.9.1/src/city_deploy/api.py +282 -0
- city_cli-0.9.1/src/city_deploy/cli.py +554 -0
- city_cli-0.9.1/src/city_deploy/clock.py +28 -0
- city_cli-0.9.1/src/city_deploy/conform.py +105 -0
- city_cli-0.9.1/src/city_deploy/errors.py +29 -0
- city_cli-0.9.1/src/city_deploy/local.py +89 -0
- city_cli-0.9.1/src/city_deploy/quota.py +48 -0
- city_cli-0.9.1/src/city_deploy/receipt.py +34 -0
- city_cli-0.9.1/src/city_deploy/runtime.py +724 -0
- city_cli-0.9.1/src/city_deploy/secrets.py +125 -0
- city_cli-0.9.1/src/city_deploy/serve.py +45 -0
- city_cli-0.9.1/src/city_deploy/service.py +943 -0
- city_cli-0.9.1/src/city_deploy/skeleton.py +298 -0
- city_cli-0.9.1/src/city_deploy/state.py +10 -0
- city_cli-0.9.1/src/city_deploy/templates/b2b/Dockerfile +9 -0
- city_cli-0.9.1/src/city_deploy/templates/b2b/SKILL.md +3 -0
- city_cli-0.9.1/src/city_deploy/templates/b2b/brain/brain.py +8 -0
- city_cli-0.9.1/src/city_deploy/templates/b2b/card.yaml +10 -0
- city_cli-0.9.1/src/city_deploy/templates/b2b/city.yaml +7 -0
- city_cli-0.9.1/src/city_deploy/templates/cup/Dockerfile +9 -0
- city_cli-0.9.1/src/city_deploy/templates/cup/SKILL.md +5 -0
- city_cli-0.9.1/src/city_deploy/templates/cup/brain/brain.py +11 -0
- city_cli-0.9.1/src/city_deploy/templates/cup/card.yaml +14 -0
- city_cli-0.9.1/src/city_deploy/templates/cup/city.yaml +7 -0
- city_cli-0.9.1/src/city_deploy/templates/facility/Dockerfile +9 -0
- city_cli-0.9.1/src/city_deploy/templates/facility/SKILL.md +3 -0
- city_cli-0.9.1/src/city_deploy/templates/facility/brain/brain.py +7 -0
- city_cli-0.9.1/src/city_deploy/templates/facility/card.yaml +16 -0
- city_cli-0.9.1/src/city_deploy/templates/facility/city.yaml +7 -0
- city_cli-0.9.1/src/city_deploy/templates/mmg/Dockerfile +9 -0
- city_cli-0.9.1/src/city_deploy/templates/mmg/SKILL.md +3 -0
- city_cli-0.9.1/src/city_deploy/templates/mmg/brain/brain.py +6 -0
- city_cli-0.9.1/src/city_deploy/templates/mmg/card.yaml +10 -0
- city_cli-0.9.1/src/city_deploy/templates/mmg/city.yaml +7 -0
- city_cli-0.9.1/src/city_index/__init__.py +3 -0
- city_cli-0.9.1/src/city_index/__main__.py +24 -0
- city_cli-0.9.1/src/city_index/app.py +109 -0
- city_cli-0.9.1/src/city_index/auth.py +15 -0
- city_cli-0.9.1/src/city_index/cache.py +43 -0
- city_cli-0.9.1/src/city_index/catalog.py +233 -0
- city_cli-0.9.1/src/city_index/events.py +38 -0
- city_cli-0.9.1/src/city_index/model.py +43 -0
- city_cli-0.9.1/src/city_index/ports.py +15 -0
- city_cli-0.9.1/src/city_index/query.py +91 -0
- city_cli-0.9.1/src/city_index/service.py +201 -0
- city_cli-0.9.1/src/city_index/visibility.py +52 -0
- city_cli-0.9.1/src/city_index/watch.py +35 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Terraform
|
|
2
|
+
**/.terraform/
|
|
3
|
+
*.tfstate
|
|
4
|
+
*.tfstate.backup
|
|
5
|
+
*.tfstate.lock.info
|
|
6
|
+
crash.log
|
|
7
|
+
crash.*.log
|
|
8
|
+
override.tf
|
|
9
|
+
override.tf.json
|
|
10
|
+
*_override.tf
|
|
11
|
+
*_override.tf.json
|
|
12
|
+
*.tfplan
|
|
13
|
+
*.tfplan.json
|
|
14
|
+
*.pyc
|
|
15
|
+
__pycache__/
|
|
16
|
+
|
|
17
|
+
# Local city CLI keystore and team token
|
|
18
|
+
.city/
|
|
19
|
+
|
|
20
|
+
# Terraform variable files with secrets — keep *.tfvars.example, ignore actual values
|
|
21
|
+
*.tfvars
|
|
22
|
+
*.tfvars.json
|
|
23
|
+
|
|
24
|
+
# Terragrunt cache
|
|
25
|
+
**/.terragrunt-cache/
|
|
26
|
+
|
|
27
|
+
# Provider lock files (optional: remove if you want to pin providers in VCS)
|
|
28
|
+
# **/.terraform.lock.hcl
|
|
29
|
+
|
|
30
|
+
# GCP service account key files
|
|
31
|
+
*.json
|
|
32
|
+
!**/cloudbuild*.json
|
|
33
|
+
# Firebase web app configs are public client identifiers (not secrets)
|
|
34
|
+
!**/firebase-config*.json
|
|
35
|
+
# Contract schemas and examples are source, not credentials
|
|
36
|
+
!specs/**/*.json
|
|
37
|
+
|
|
38
|
+
# macOS
|
|
39
|
+
.DS_Store
|
|
40
|
+
.AppleDouble
|
|
41
|
+
.LSOverride
|
|
42
|
+
|
|
43
|
+
# Editor
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
*~
|
|
49
|
+
|
|
50
|
+
# Node.js / Next.js config files
|
|
51
|
+
!package-lock.json
|
|
52
|
+
!package.json
|
|
53
|
+
!tsconfig.json
|
|
54
|
+
!next.config.mjs
|
|
55
|
+
!next.config.js
|
|
56
|
+
node_modules/
|
|
57
|
+
services/ui/dist/
|
|
58
|
+
services/ui/.env
|
|
59
|
+
services/ui/.env.local
|
|
60
|
+
services/ui/.env.*.local
|
|
61
|
+
.venv/
|
|
62
|
+
.mypy_cache/
|
|
63
|
+
.ruff_cache/
|
|
64
|
+
.pytest_cache/
|
|
65
|
+
|
|
66
|
+
# Elementor kit workdir / local backups (keep the published zip + README tracked)
|
|
67
|
+
landing/elementor-export/.kit-work/
|
|
68
|
+
landing/elementor-export/*.bak.zip
|
|
69
|
+
landing/elementor-export/migrate_to_native.py
|
|
70
|
+
landing/elementor-export/pni-elementor-kit.zip
|
|
71
|
+
landing/elementor-export/README.md
|
|
72
|
+
.publish/
|
|
73
|
+
dist-pypi/
|
city_cli-0.9.1/PKG-INFO
ADDED
|
@@ -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.
|
city_cli-0.9.1/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# city-cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for **Milano Agentic City** teams.
|
|
4
|
+
|
|
5
|
+
Install once, then scaffold an agent, run it locally, and publish it through the City Deploy API — **without GCP credentials** (ADR-010).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python **3.12+**
|
|
10
|
+
- For live deploy: Deploy API URL (see below)
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pipx install city-cli
|
|
16
|
+
# or
|
|
17
|
+
uv tool install city-cli
|
|
18
|
+
# or
|
|
19
|
+
pip install city-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This provides the `city` command. It depends on [`city-sdk`](https://pypi.org/project/city-sdk/) for the Market facade and harness types.
|
|
23
|
+
|
|
24
|
+
## Live city (remote Deploy API)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export CITY_DEPLOY_URL=https://deploy.projectnandaitaly.ai
|
|
28
|
+
|
|
29
|
+
city login --team t07
|
|
30
|
+
city keys init
|
|
31
|
+
city init --role cup --name demo
|
|
32
|
+
city card validate
|
|
33
|
+
city deploy --promote
|
|
34
|
+
city status agent:t07/demo
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Per-command override:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
city --api-url https://deploy.projectnandaitaly.ai status agent:t07/demo
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
JSON for scripting:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
city --json --api-url "$CITY_DEPLOY_URL" status agent:t07/demo
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Local kit (no API URL)
|
|
50
|
+
|
|
51
|
+
If `CITY_DEPLOY_URL` / `--api-url` is unset, `city` uses an **in-process** Deploy service (useful for `init`, `dev`, and offline checks).
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
city init --role cup --name demo
|
|
55
|
+
city dev
|
|
56
|
+
city conform --profile fast
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands (overview)
|
|
60
|
+
|
|
61
|
+
| Command | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `city login` | Device-flow team session; stores token under `.city/` |
|
|
64
|
+
| `city keys init` | Local controller key + register public key |
|
|
65
|
+
| `city init` | Scaffold `cup` / `facility` / `mmg` / `b2b` template |
|
|
66
|
+
| `city card validate` | Basic card.yaml checks |
|
|
67
|
+
| `city dev` | One local need through the harness |
|
|
68
|
+
| `city conform` | Conformance suite (`--profile full\|fast`, or `--url`) |
|
|
69
|
+
| `city deploy` | Upload + pipeline (`--promote`, `--runtime`) |
|
|
70
|
+
| `city promote` / `rollback` / `status` / `retire` | Lifecycle |
|
|
71
|
+
| `city secrets set` / `city scale` / `city llm key rotate` | Ops |
|
|
72
|
+
| `city compliance` / `city verify` | Receipts and offline verify |
|
|
73
|
+
|
|
74
|
+
## Links
|
|
75
|
+
|
|
76
|
+
- Source: [projectnandaitaly/agentic-city](https://github.com/projectnandaitaly/agentic-city)
|
|
77
|
+
- SDK: [city-sdk on PyPI](https://pypi.org/project/city-sdk/)
|
|
78
|
+
- Spec: City Deploy CLI §10.2 in the repo docs
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Proprietary — Project NANDA Italy / Milano Agentic City event kit.
|
city_cli-0.9.1/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.9.1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "city-cli"
|
|
3
|
+
version = "0.9.1"
|
|
4
|
+
description = "Milano Agentic City CLI (city): init, dev, conform, and deploy."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "Proprietary" }
|
|
8
|
+
authors = [{ name = "Project NANDA Italy" }]
|
|
9
|
+
keywords = ["agentic-city", "cli", "deploy", "nanda"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Topic :: Software Development :: Libraries",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"city-sdk>=0.9.1",
|
|
21
|
+
"cryptography>=43",
|
|
22
|
+
"fastapi>=0.115",
|
|
23
|
+
"httpx>=0.27",
|
|
24
|
+
"pydantic>=2",
|
|
25
|
+
"rfc8785>=0.1.4",
|
|
26
|
+
"typer>=0.12",
|
|
27
|
+
"uvicorn>=0.32",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/projectnandaitaly/agentic-city"
|
|
32
|
+
Repository = "https://github.com/projectnandaitaly/agentic-city"
|
|
33
|
+
Documentation = "https://github.com/projectnandaitaly/agentic-city/blob/main/services/deploy/README.md"
|
|
34
|
+
Issues = "https://github.com/projectnandaitaly/agentic-city/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
city = "city_deploy.cli:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/city_deploy", "src/city_conform", "src/city_index", "src/city"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""City libraries (``city.testkit``, later reputation and others)."""
|
|
File without changes
|
|
@@ -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)
|
|
@@ -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
|
+
}
|