city-sdk 0.9.0__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.
Files changed (54) hide show
  1. city_sdk-0.9.0/.gitignore +72 -0
  2. city_sdk-0.9.0/PKG-INFO +11 -0
  3. city_sdk-0.9.0/pyproject.toml +23 -0
  4. city_sdk-0.9.0/src/cap/__init__.py +1 -0
  5. city_sdk-0.9.0/src/cap/core/__init__.py +32 -0
  6. city_sdk-0.9.0/src/cap/core/envelope.py +156 -0
  7. city_sdk-0.9.0/src/cap/core/errors.py +59 -0
  8. city_sdk-0.9.0/src/cap/core/generated/__init__.py +1 -0
  9. city_sdk-0.9.0/src/cap/core/generated/clients/agent.py +163 -0
  10. city_sdk-0.9.0/src/cap/core/generated/clients/attestation.py +104 -0
  11. city_sdk-0.9.0/src/cap/core/generated/clients/deploy.py +107 -0
  12. city_sdk-0.9.0/src/cap/core/generated/clients/governance/__init__.py +47 -0
  13. city_sdk-0.9.0/src/cap/core/generated/clients/governance/city/__init__.py +3 -0
  14. city_sdk-0.9.0/src/cap/core/generated/clients/governance/city/order.py +22 -0
  15. city_sdk-0.9.0/src/cap/core/generated/clients/index.py +95 -0
  16. city_sdk-0.9.0/src/cap/core/generated/clients/ledger.py +22 -0
  17. city_sdk-0.9.0/src/cap/core/generated/clients/market.py +101 -0
  18. city_sdk-0.9.0/src/cap/core/generated/clients/relay.py +23 -0
  19. city_sdk-0.9.0/src/cap/core/generated/events.py +46 -0
  20. city_sdk-0.9.0/src/cap/core/generated/models/__init__.py +3 -0
  21. city_sdk-0.9.0/src/cap/core/generated/models/ack.py +23 -0
  22. city_sdk-0.9.0/src/cap/core/generated/models/cap_probe.py +13 -0
  23. city_sdk-0.9.0/src/cap/core/generated/models/cap_probe_result.py +13 -0
  24. city_sdk-0.9.0/src/cap/core/generated/models/card.py +49 -0
  25. city_sdk-0.9.0/src/cap/core/generated/models/city_order_ack.py +22 -0
  26. city_sdk-0.9.0/src/cap/core/generated/models/envelope.py +41 -0
  27. city_sdk-0.9.0/src/cap/core/generated/models/error.py +16 -0
  28. city_sdk-0.9.0/src/cap/core/generated/models/error_codes.py +27 -0
  29. city_sdk-0.9.0/src/cap/core/generated/models/grant.py +26 -0
  30. city_sdk-0.9.0/src/cap/core/generated/models/order.py +20 -0
  31. city_sdk-0.9.0/src/cap/core/generated/models/order_kinds.py +19 -0
  32. city_sdk-0.9.0/src/cap/core/grant.py +83 -0
  33. city_sdk-0.9.0/src/cap/core/http_ports.py +95 -0
  34. city_sdk-0.9.0/src/cap/core/jcs.py +38 -0
  35. city_sdk-0.9.0/src/cap/core/jws.py +62 -0
  36. city_sdk-0.9.0/src/cap/core/keys.py +97 -0
  37. city_sdk-0.9.0/src/cap/core/process.py +35 -0
  38. city_sdk-0.9.0/src/cap/core/py.typed +0 -0
  39. city_sdk-0.9.0/src/cap/core/timeutil.py +28 -0
  40. city_sdk-0.9.0/src/city_sdk/__init__.py +3 -0
  41. city_sdk-0.9.0/src/city_sdk/__main__.py +31 -0
  42. city_sdk-0.9.0/src/city_sdk/adapters.py +17 -0
  43. city_sdk-0.9.0/src/city_sdk/app.py +51 -0
  44. city_sdk-0.9.0/src/city_sdk/boot.py +268 -0
  45. city_sdk-0.9.0/src/city_sdk/cli.py +57 -0
  46. city_sdk-0.9.0/src/city_sdk/harness.py +379 -0
  47. city_sdk-0.9.0/src/city_sdk/http.py +223 -0
  48. city_sdk-0.9.0/src/city_sdk/journal.py +59 -0
  49. city_sdk-0.9.0/src/city_sdk/layers.py +21 -0
  50. city_sdk-0.9.0/src/city_sdk/llm.py +74 -0
  51. city_sdk-0.9.0/src/city_sdk/logs.py +55 -0
  52. city_sdk-0.9.0/src/city_sdk/market.py +150 -0
  53. city_sdk-0.9.0/src/city_sdk/policy.py +23 -0
  54. city_sdk-0.9.0/src/city_sdk/ports.py +126 -0
@@ -0,0 +1,72 @@
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/
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: city-sdk
3
+ Version: 0.9.0
4
+ Summary: City harness and the day-1 Market facade (includes cap).
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: cryptography>=43
7
+ Requires-Dist: fastapi>=0.115
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: pydantic>=2
10
+ Requires-Dist: rfc8785>=0.1.4
11
+ Requires-Dist: uvicorn>=0.32
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "city-sdk"
3
+ version = "0.9.0"
4
+ description = "City harness and the day-1 Market facade (includes cap)."
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "cryptography>=43",
8
+ "fastapi>=0.115",
9
+ "httpx>=0.27",
10
+ "pydantic>=2",
11
+ "rfc8785>=0.1.4",
12
+ "uvicorn>=0.32",
13
+ ]
14
+
15
+ [project.scripts]
16
+ city-sdk = "city_sdk.__main__:main"
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/city_sdk", "src/cap"]
@@ -0,0 +1 @@
1
+ """CAP library namespace (``cap.core``, later ``cap.sdk``, …)."""
@@ -0,0 +1,32 @@
1
+ """CAP canonicalization, signatures, grants, envelopes, and error codes."""
2
+
3
+ from cap.core.envelope import build, sign, verify
4
+ from cap.core.errors import CapError, http_status_for
5
+ from cap.core.grant import SignedGrant
6
+ from cap.core.grant import issue as issue_grant
7
+ from cap.core.grant import verify as verify_grant
8
+ from cap.core.jcs import canonicalize, card_digest, digest
9
+ from cap.core.jws import sign_detached, verify_detached
10
+ from cap.core.keys import KeyPair, PrivateKey, PublicKey, generate
11
+
12
+ __version__ = "0.9.0"
13
+
14
+ __all__ = [
15
+ "CapError",
16
+ "KeyPair",
17
+ "PrivateKey",
18
+ "PublicKey",
19
+ "SignedGrant",
20
+ "build",
21
+ "canonicalize",
22
+ "card_digest",
23
+ "digest",
24
+ "generate",
25
+ "http_status_for",
26
+ "issue_grant",
27
+ "sign",
28
+ "sign_detached",
29
+ "verify",
30
+ "verify_detached",
31
+ "verify_grant",
32
+ ]
@@ -0,0 +1,156 @@
1
+ """Build, sign, and verify a CAP envelope."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from datetime import datetime, timedelta
7
+
8
+ from cap.core.errors import CapError
9
+ from cap.core.grant import SignedGrant
10
+ from cap.core.grant import verify as verify_grant
11
+ from cap.core.jcs import canonicalize
12
+ from cap.core.jws import sign_detached, verify_detached
13
+ from cap.core.keys import PrivateKey, PublicKey
14
+ from cap.core.timeutil import format_time, parse_time
15
+
16
+ _SKEW = timedelta(seconds=120)
17
+ _DEFAULT_TTL = timedelta(seconds=60)
18
+
19
+
20
+ def build(
21
+ *,
22
+ msg_id: str,
23
+ message_type: str,
24
+ sender: str,
25
+ recipient: str,
26
+ sent_at: datetime,
27
+ body: Mapping[str, object],
28
+ profile: str | None = None,
29
+ expires_at: datetime | None = None,
30
+ conversation_id: str | None = None,
31
+ need_id: str | None = None,
32
+ in_reply_to: str | None = None,
33
+ traceparent: str | None = None,
34
+ ) -> dict[str, object]:
35
+ envelope: dict[str, object] = {
36
+ "cap": "1.0",
37
+ "msg_id": msg_id,
38
+ "type": message_type,
39
+ "from": sender,
40
+ "to": recipient,
41
+ "sent_at": format_time(sent_at),
42
+ "body": dict(body),
43
+ }
44
+ if profile is not None:
45
+ envelope["profile"] = profile
46
+ if expires_at is not None:
47
+ envelope["expires_at"] = format_time(expires_at)
48
+ if conversation_id is not None:
49
+ envelope["conversation_id"] = conversation_id
50
+ if need_id is not None:
51
+ envelope["need_id"] = need_id
52
+ if in_reply_to is not None:
53
+ envelope["in_reply_to"] = in_reply_to
54
+ if traceparent is not None:
55
+ envelope["traceparent"] = traceparent
56
+ return envelope
57
+
58
+
59
+ def sign(
60
+ envelope: Mapping[str, object],
61
+ private: PrivateKey,
62
+ *,
63
+ kid: str,
64
+ grant_id: str | None = None,
65
+ ) -> dict[str, object]:
66
+ unsigned = {key: value for key, value in envelope.items() if key != "sig"}
67
+ sender = unsigned.get("from")
68
+ if not isinstance(sender, str) or not kid.startswith(sender + "#"):
69
+ raise CapError("KEY_UNKNOWN", "kid is not under from")
70
+ sig: dict[str, str] = {"alg": "EdDSA", "kid": kid}
71
+ if grant_id is not None:
72
+ sig["grant"] = grant_id
73
+ sig["jws"] = sign_detached(private, canonicalize(unsigned).encode("utf-8"), kid=kid)
74
+ signed = dict(unsigned)
75
+ signed["sig"] = sig
76
+ return signed
77
+
78
+
79
+ def verify(
80
+ envelope: Mapping[str, object],
81
+ *,
82
+ now: datetime,
83
+ key_for_kid: Callable[[str], PublicKey],
84
+ grant_for_id: Callable[[str], SignedGrant] | None = None,
85
+ controller_public: PublicKey | None = None,
86
+ required_permission: str | None = None,
87
+ target_visible: Callable[[str], bool] | None = None,
88
+ record_verifier: Callable[[Mapping[str, object]], None] | None = None,
89
+ ) -> None:
90
+ """Verify signature, ``from``/``kid``, grant chain, expiry, and clock skew.
91
+
92
+ ``target_visible`` defaults to true. The Relay supplies the Index check.
93
+ """
94
+ if now.tzinfo is None:
95
+ raise CapError("SCHEMA_INVALID", "now must be timezone-aware")
96
+ msg_id = envelope.get("msg_id")
97
+ msg = msg_id if isinstance(msg_id, str) else None
98
+ unsigned = {key: value for key, value in envelope.items() if key != "sig"}
99
+ for field in ("cap", "msg_id", "type", "from", "to", "sent_at", "body"):
100
+ if field not in unsigned:
101
+ raise CapError("SCHEMA_INVALID", f"missing {field}", msg)
102
+ if unsigned.get("cap") != "1.0":
103
+ raise CapError("SCHEMA_INVALID", "cap must be 1.0", msg)
104
+ sender = unsigned["from"]
105
+ recipient = unsigned["to"]
106
+ if not isinstance(sender, str) or not isinstance(recipient, str):
107
+ raise CapError("SCHEMA_INVALID", "from and to must be strings", msg)
108
+ sig = envelope.get("sig")
109
+ if not isinstance(sig, Mapping):
110
+ raise CapError("SCHEMA_INVALID", "sig is required", msg)
111
+ if sig.get("alg") != "EdDSA":
112
+ raise CapError("SIGNATURE_INVALID", "sig.alg must be EdDSA", msg)
113
+ kid = sig.get("kid")
114
+ token = sig.get("jws")
115
+ if not isinstance(kid, str) or not isinstance(token, str):
116
+ raise CapError("SCHEMA_INVALID", "sig.kid and sig.jws are required", msg)
117
+ if not kid.startswith(sender + "#"):
118
+ raise CapError("KEY_UNKNOWN", "kid is not under from", msg)
119
+ try:
120
+ public = key_for_kid(kid)
121
+ except KeyError as exc:
122
+ raise CapError("KEY_UNKNOWN", kid, msg) from exc
123
+ verify_detached(public, canonicalize(unsigned).encode("utf-8"), token, kid=kid)
124
+ grant_id = sig.get("grant")
125
+ if grant_id is not None:
126
+ if not isinstance(grant_id, str):
127
+ raise CapError("SCHEMA_INVALID", "sig.grant must be a string", msg)
128
+ if grant_for_id is None or controller_public is None:
129
+ raise CapError("SIGNATURE_INVALID", "grant resolver is required", msg)
130
+ try:
131
+ signed_grant = grant_for_id(grant_id)
132
+ except KeyError as exc:
133
+ raise CapError("SIGNATURE_INVALID", "unknown grant", msg) from exc
134
+ if signed_grant.grant.get("grant_id") != grant_id:
135
+ raise CapError("SIGNATURE_INVALID", "grant_id mismatch", msg)
136
+ if signed_grant.grant.get("agent_id") != sender:
137
+ raise CapError("SIGNATURE_INVALID", "grant agent_id mismatch", msg)
138
+ if signed_grant.grant.get("agent_pubkey") != public.to_jwk():
139
+ raise CapError("KEY_UNKNOWN", "grant key does not match kid", msg)
140
+ verify_grant(
141
+ signed_grant,
142
+ controller_public,
143
+ now=now,
144
+ required_permission=required_permission,
145
+ record_verifier=record_verifier,
146
+ )
147
+ sent_at = parse_time(unsigned["sent_at"])
148
+ if abs(now - sent_at) > _SKEW:
149
+ raise CapError("EXPIRED", "sent_at outside ±120s", msg)
150
+ expires_raw = unsigned.get("expires_at")
151
+ expires_at = parse_time(expires_raw) if expires_raw is not None else sent_at + _DEFAULT_TTL
152
+ if now > expires_at:
153
+ raise CapError("EXPIRED", "expires_at passed", msg)
154
+ visible = True if target_visible is None else target_visible(recipient)
155
+ if not visible:
156
+ raise CapError("TARGET_NOT_VISIBLE", recipient, msg)
@@ -0,0 +1,59 @@
1
+ """City error codes and the CAP error body (``specs/cap/error.json``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # specification 04 §6.9. Profile codes are listed there and are not in error-codes.json.
6
+ HTTP_STATUS: dict[str, int] = {
7
+ "SCHEMA_INVALID": 400,
8
+ "SIGNATURE_INVALID": 401,
9
+ "KEY_UNKNOWN": 401,
10
+ "GRANT_EXPIRED": 401,
11
+ "GRANT_PERMISSION_DENIED": 403,
12
+ "TARGET_NOT_VISIBLE": 404,
13
+ "IDEMPOTENCY_CONFLICT": 400,
14
+ "DUPLICATE": 409,
15
+ "EXPIRED": 410,
16
+ "POLICY_VIOLATION": 422,
17
+ "STALE_FENCE": 422,
18
+ "STALE_EPOCH": 422,
19
+ "OUT_OF_SCOPE": 422,
20
+ "NO_CAPACITY": 422,
21
+ "CONSENT_MISSING": 422,
22
+ "RATE_LIMITED": 429,
23
+ "DRAINING": 503,
24
+ "QUARANTINED": 503,
25
+ "SUSPENDED": 503,
26
+ "UPSTREAM_TIMEOUT": 504,
27
+ "AGENT_LOST_CUSTODY": 502,
28
+ }
29
+
30
+
31
+ class CapError(Exception):
32
+ """A CAP failure. ``body()`` matches ``specs/cap/error.json``."""
33
+
34
+ def __init__(self, code: str, message: str = "", msg_id: str | None = None) -> None:
35
+ self.code = code
36
+ self.message = message
37
+ self.msg_id = msg_id
38
+ super().__init__(code if not message else f"{code}: {message}")
39
+
40
+ def http_status(self) -> int:
41
+ try:
42
+ return HTTP_STATUS[self.code]
43
+ except KeyError as exc:
44
+ raise CapError("SCHEMA_INVALID", f"unknown error code {self.code}") from exc
45
+
46
+ def body(self) -> dict[str, str]:
47
+ payload: dict[str, str] = {"code": self.code}
48
+ if self.message:
49
+ payload["message"] = self.message
50
+ if self.msg_id is not None:
51
+ payload["msg_id"] = self.msg_id
52
+ return payload
53
+
54
+
55
+ def http_status_for(code: str) -> int:
56
+ try:
57
+ return HTTP_STATUS[code]
58
+ except KeyError as exc:
59
+ raise CapError("SCHEMA_INVALID", f"unknown error code {code}") from exc
@@ -0,0 +1 @@
1
+ """Models produced by ``make gen``. Do not edit by hand."""
@@ -0,0 +1,163 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: agent.yaml
3
+ # timestamp: 2026-10-10T14:29:54+00:00
4
+
5
+ from __future__ import annotations
6
+ from enum import Enum, StrEnum
7
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel, conint, constr
8
+ from typing import Any, Literal
9
+ from uuid import UUID
10
+
11
+
12
+ class Status(Enum):
13
+ ok = "ok"
14
+ degraded = "degraded"
15
+ draining = "draining"
16
+ quarantined = "quarantined"
17
+
18
+
19
+ class Policy(BaseModel):
20
+ epoch: int
21
+ digest: str
22
+ applied_at: AwareDatetime | None = None
23
+
24
+
25
+ class Health(BaseModel):
26
+ model_config = ConfigDict(
27
+ extra="forbid",
28
+ )
29
+ status: Status
30
+ agent_id: str
31
+ version: str
32
+ cap_versions: list[str]
33
+ profiles: list[str]
34
+ card_digest: str
35
+ policy: Policy | None = None
36
+ load: dict[str, Any] | None = None
37
+ build: dict[str, Any] | None = None
38
+ models: list[dict[str, Any]] | None = None
39
+ uptime_s: conint(ge=0)
40
+
41
+
42
+ class Delivery1(BaseModel):
43
+ model_config = ConfigDict(
44
+ extra="forbid",
45
+ )
46
+ msg_id: UUID
47
+ mailbox: str
48
+ fence: conint(ge=1)
49
+ attempt: conint(ge=1)
50
+ lease_until: AwareDatetime
51
+
52
+
53
+ class Sig(BaseModel):
54
+ model_config = ConfigDict(
55
+ extra="forbid",
56
+ )
57
+ alg: Literal["EdDSA"]
58
+ kid: str
59
+ grant: str | None = None
60
+ jws: str
61
+
62
+
63
+ class Envelope(BaseModel):
64
+ model_config = ConfigDict(
65
+ extra="forbid",
66
+ )
67
+ cap: Literal["1.0"]
68
+ profile: constr(pattern=r"^[a-z][a-z0-9-]*/[0-9]+\.[0-9]+$") | None = None
69
+ msg_id: UUID
70
+ type: constr(pattern=r"^[a-z][a-z0-9.-]*$")
71
+ from_: constr(pattern=r"^[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._/-]*$") = Field(
72
+ ..., alias="from"
73
+ )
74
+ to: constr(pattern=r"^[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._/-]*$")
75
+ conversation_id: constr(max_length=128) | None = None
76
+ need_id: constr(max_length=128) | None = None
77
+ in_reply_to: UUID | None = None
78
+ fence: conint(ge=1) | None = None
79
+ sent_at: AwareDatetime
80
+ expires_at: AwareDatetime | None = None
81
+ traceparent: constr(pattern=r"^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$") | None = None
82
+ body: dict[str, Any]
83
+ sig: Sig
84
+
85
+
86
+ class Status1(Enum):
87
+ processed = "processed"
88
+ received = "received"
89
+ rejected = "rejected"
90
+ retryable = "retryable"
91
+
92
+
93
+ class DeliveryAck(BaseModel):
94
+ status: Status1
95
+ envelopes: list[Envelope] | None = None
96
+
97
+
98
+ class Probe(StrEnum):
99
+ live = "live"
100
+ ready = "ready"
101
+
102
+
103
+ class Error(BaseModel):
104
+ model_config = ConfigDict(
105
+ extra="forbid",
106
+ )
107
+ code: constr(pattern=r"^[A-Z][A-Z0-9_]*$")
108
+ message: str | None = None
109
+ msg_id: UUID | None = None
110
+
111
+
112
+ class Profile(RootModel[constr(pattern=r"^[a-z][a-z0-9-]*/[0-9]+\.[0-9]+$")]):
113
+ root: constr(pattern=r"^[a-z][a-z0-9-]*/[0-9]+\.[0-9]+$")
114
+
115
+
116
+ class Endpoints(BaseModel):
117
+ model_config = ConfigDict(
118
+ extra="forbid",
119
+ )
120
+ mailbox: str
121
+ send_via: str | None = None
122
+
123
+
124
+ class Key(BaseModel):
125
+ model_config = ConfigDict(
126
+ extra="forbid",
127
+ )
128
+ kid: str
129
+ kty: Literal["OKP"]
130
+ crv: Literal["Ed25519"]
131
+ x: str
132
+
133
+
134
+ class Card(BaseModel):
135
+ agent_id: constr(pattern=r"^agent:[a-z0-9-]+/[a-z0-9-]+$")
136
+ team: constr(pattern=r"^team:[a-z0-9-]+$")
137
+ name: str
138
+ version: constr(pattern=r"^[0-9]+\.[0-9]+\.[0-9]+$")
139
+ cap_versions: list[Literal["1.0"]] = Field(..., min_length=1)
140
+ profiles: list[Profile] = Field(..., min_length=1)
141
+ data_processing: list[dict[str, Any]] | None = None
142
+ ai_act_transparency: str | None = None
143
+ governable: list[str] | None = None
144
+ max_inflight: conint(ge=1) | None = None
145
+ consent: dict[str, Any] | None = None
146
+ build: dict[str, Any] | None = None
147
+ dependencies: dict[str, Any] | None = None
148
+ endpoints: Endpoints
149
+ keys: list[Key] = Field(..., min_length=1)
150
+ grant: str | None = None
151
+ signatures: dict[str, Any] | None = None
152
+
153
+
154
+ class AuditGetResponse(BaseModel):
155
+ entries: list[dict[str, Any]]
156
+
157
+
158
+ class Delivery(BaseModel):
159
+ model_config = ConfigDict(
160
+ extra="forbid",
161
+ )
162
+ delivery: Delivery1
163
+ envelope: Envelope
@@ -0,0 +1,104 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: attestation.yaml
3
+ # timestamp: 2026-10-10T14:29:54+00:00
4
+
5
+ from __future__ import annotations
6
+ from typing import Any, Literal
7
+ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel, conint, constr
8
+ from uuid import UUID
9
+
10
+
11
+ class CityEvent1(BaseModel):
12
+ model_config = ConfigDict(
13
+ extra="allow",
14
+ )
15
+ record_kind: Literal["intent"]
16
+ record_id: str
17
+ observer: str
18
+ actor: str
19
+ subject: str | None = None
20
+ action: str
21
+ msg_type: str | None = None
22
+ msg_id: str | None = None
23
+ conversation_id: str | None = None
24
+ need_id: str | None = None
25
+ trace_id: str | None = None
26
+ time: AwareDatetime
27
+ envelope_digest: str | None = None
28
+ envelope_sig: str | None = None
29
+ payload_ref: str | None = None
30
+
31
+
32
+ class CityEvent2(BaseModel):
33
+ model_config = ConfigDict(
34
+ extra="allow",
35
+ )
36
+ record_kind: Literal["event"]
37
+ record_id: str
38
+ observer: str
39
+ type: str
40
+ about_record: str | None = None
41
+ outcome: dict[str, Any] | None = None
42
+ transport_policy: str | None = None
43
+ time: AwareDatetime
44
+
45
+
46
+ class CityEvent(RootModel[CityEvent1 | CityEvent2]):
47
+ root: CityEvent1 | CityEvent2 = Field(..., title="city-events record")
48
+
49
+
50
+ class Sig(BaseModel):
51
+ model_config = ConfigDict(
52
+ extra="forbid",
53
+ )
54
+ alg: Literal["EdDSA"]
55
+ kid: str
56
+ grant: str | None = None
57
+ jws: str
58
+
59
+
60
+ class Envelope(BaseModel):
61
+ model_config = ConfigDict(
62
+ extra="forbid",
63
+ )
64
+ cap: Literal["1.0"]
65
+ profile: constr(pattern=r"^[a-z][a-z0-9-]*/[0-9]+\.[0-9]+$") | None = None
66
+ msg_id: UUID
67
+ type: constr(pattern=r"^[a-z][a-z0-9.-]*$")
68
+ from_: constr(pattern=r"^[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._/-]*$") = Field(
69
+ ..., alias="from"
70
+ )
71
+ to: constr(pattern=r"^[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._/-]*$")
72
+ conversation_id: constr(max_length=128) | None = None
73
+ need_id: constr(max_length=128) | None = None
74
+ in_reply_to: UUID | None = None
75
+ fence: conint(ge=1) | None = None
76
+ sent_at: AwareDatetime
77
+ expires_at: AwareDatetime | None = None
78
+ traceparent: constr(pattern=r"^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$") | None = None
79
+ body: dict[str, Any]
80
+ sig: Sig
81
+
82
+
83
+ class V1ProofRecordIdGetResponse(BaseModel):
84
+ record_id: str
85
+ seal: str
86
+ proof: list[str]
87
+
88
+
89
+ class Seal(BaseModel):
90
+ seal_id: str
91
+ prev: str
92
+ root: str
93
+
94
+
95
+ class V1SealsGetResponse(BaseModel):
96
+ seals: list[Seal]
97
+
98
+
99
+ class V1RecordsGetResponse(BaseModel):
100
+ records: list[CityEvent]
101
+
102
+
103
+ class V1ConversationsIdGetResponse(BaseModel):
104
+ envelopes: list[Envelope]