msgctl 0.1.0a1__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 (176) hide show
  1. msg/__init__.py +2 -0
  2. msg/admin/__init__.py +0 -0
  3. msg/admin/backup_retirement.py +56 -0
  4. msg/admin/backups.py +341 -0
  5. msg/admin/custodial_check.py +77 -0
  6. msg/admin/diagnostics.py +750 -0
  7. msg/admin/market.py +91 -0
  8. msg/admin/market_check.py +341 -0
  9. msg/admin/money.py +322 -0
  10. msg/admin/preflight.py +142 -0
  11. msg/admin/recovery_replay.py +281 -0
  12. msg/admin/restore_database.py +37 -0
  13. msg/admin/root.py +347 -0
  14. msg/admin/rotation.py +197 -0
  15. msg/admin/token_delivery_check.py +54 -0
  16. msg/admin/upgrade_check.py +58 -0
  17. msg/application.py +163 -0
  18. msg/bootstrap.py +305 -0
  19. msg/cli.py +537 -0
  20. msg/client.py +725 -0
  21. msg/client_certificates.py +98 -0
  22. msg/client_content.py +43 -0
  23. msg/client_custodial.py +206 -0
  24. msg/client_market.py +104 -0
  25. msg/client_recovery.py +186 -0
  26. msg/client_secrets.py +58 -0
  27. msg/client_tokens.py +99 -0
  28. msg/client_upgrade.py +188 -0
  29. msg/config.py +318 -0
  30. msg/constants.py +11 -0
  31. msg/core/__init__.py +0 -0
  32. msg/core/batching.py +23 -0
  33. msg/core/codec.py +253 -0
  34. msg/core/contracts.py +128 -0
  35. msg/core/cursors.py +68 -0
  36. msg/core/email_address.py +33 -0
  37. msg/core/errors.py +22 -0
  38. msg/core/events.py +6 -0
  39. msg/core/execution_ports.py +20 -0
  40. msg/core/executor.py +193 -0
  41. msg/core/models.py +517 -0
  42. msg/core/packet.py +71 -0
  43. msg/core/permissions.py +14 -0
  44. msg/core/query.py +44 -0
  45. msg/core/read_query.py +60 -0
  46. msg/core/registry.py +173 -0
  47. msg/core/requests.py +58 -0
  48. msg/core/schema_policy.py +28 -0
  49. msg/core/schemas.py +30 -0
  50. msg/core/tags.py +21 -0
  51. msg/core/template_dsl.py +94 -0
  52. msg/core/text_patch.py +283 -0
  53. msg/core/tool_execution.py +28 -0
  54. msg/core/transfer.py +183 -0
  55. msg/daemon.py +255 -0
  56. msg/data/__init__.py +0 -0
  57. msg/data/bootstrap.json +72 -0
  58. msg/data/favicon.png +0 -0
  59. msg/data/logo-dark.svg +5 -0
  60. msg/data/logo.svg +5 -0
  61. msg/data/recovery-checkpoint.example.json +1 -0
  62. msg/data/recovery-checkpoint.schema.json +118 -0
  63. msg/data/shortcodes.json +1 -0
  64. msg/data/system/AGENTS.md +6 -0
  65. msg/data/system/rules/_index.md +17 -0
  66. msg/data/system/rules/auth.md +6 -0
  67. msg/data/system/rules/files.md +6 -0
  68. msg/data/system/rules/identity.md +6 -0
  69. msg/data/system/rules/protocol.md +6 -0
  70. msg/data/system/rules/read-write.md +6 -0
  71. msg/data/system/rules/recovery.md +6 -0
  72. msg/data/system/rules/security.md +10 -0
  73. msg/data/system/rules/topics.md +6 -0
  74. msg/extensions/__init__.py +1 -0
  75. msg/extensions/hosting.py +259 -0
  76. msg/extensions/keystore.py +96 -0
  77. msg/extensions/repositories.py +769 -0
  78. msg/extensions/rss.py +67 -0
  79. msg/extensions/ssh.py +264 -0
  80. msg/extensions/ssh_git.py +300 -0
  81. msg/extensions/tools.py +150 -0
  82. msg/hosting_runtime.py +138 -0
  83. msg/market/__init__.py +1 -0
  84. msg/market/arbitration.py +481 -0
  85. msg/market/delivery.py +338 -0
  86. msg/market/delivery_notifications.py +110 -0
  87. msg/market/delivery_targets.py +76 -0
  88. msg/market/email.py +100 -0
  89. msg/market/escrow.py +345 -0
  90. msg/market/orders.py +218 -0
  91. msg/market/policy.py +147 -0
  92. msg/market/rationale.py +91 -0
  93. msg/market/references.py +19 -0
  94. msg/market/targets.py +157 -0
  95. msg/plugins/__init__.py +28 -0
  96. msg/plugins/achievements.py +316 -0
  97. msg/plugins/batch.py +32 -0
  98. msg/plugins/bounty.py +366 -0
  99. msg/plugins/collaboration.py +261 -0
  100. msg/plugins/common.py +217 -0
  101. msg/plugins/communication.py +853 -0
  102. msg/plugins/content.py +855 -0
  103. msg/plugins/custodial_lifecycle.py +224 -0
  104. msg/plugins/delivery.py +230 -0
  105. msg/plugins/discovery.py +1266 -0
  106. msg/plugins/discussion.py +162 -0
  107. msg/plugins/extensions.py +10 -0
  108. msg/plugins/following.py +78 -0
  109. msg/plugins/hosting_capacity.py +52 -0
  110. msg/plugins/identity.py +1654 -0
  111. msg/plugins/money.py +221 -0
  112. msg/plugins/offers.py +183 -0
  113. msg/plugins/orders.py +273 -0
  114. msg/plugins/recovery.py +376 -0
  115. msg/plugins/schemas.py +5 -0
  116. msg/plugins/sharing.py +300 -0
  117. msg/plugins/store.py +249 -0
  118. msg/plugins/system.py +75 -0
  119. msg/plugins/transfer.py +249 -0
  120. msg/py.typed +0 -0
  121. msg/security/__init__.py +0 -0
  122. msg/security/age_keys.py +112 -0
  123. msg/security/authentication.py +141 -0
  124. msg/security/authorization.py +300 -0
  125. msg/security/backup_retirement.py +135 -0
  126. msg/security/capabilities.py +157 -0
  127. msg/security/certificates.py +183 -0
  128. msg/security/crypto.py +102 -0
  129. msg/security/custodial_migration.py +292 -0
  130. msg/security/custody_history.py +146 -0
  131. msg/security/network.py +81 -0
  132. msg/security/policy.py +67 -0
  133. msg/security/quarantine.py +19 -0
  134. msg/security/root_files.py +67 -0
  135. msg/security/rotation_journal.py +60 -0
  136. msg/security/sealed_box.py +70 -0
  137. msg/security/sharing_policy.py +29 -0
  138. msg/security/token_delivery.py +93 -0
  139. msg/security/vault.py +186 -0
  140. msg/storage/__init__.py +0 -0
  141. msg/storage/capacity.py +100 -0
  142. msg/storage/custodial_migration.py +22 -0
  143. msg/storage/git.py +435 -0
  144. msg/storage/ledger_migration.py +307 -0
  145. msg/storage/market_migration.py +49 -0
  146. msg/storage/postgres.py +663 -0
  147. msg/storage/query.py +39 -0
  148. msg/storage/read_only.py +126 -0
  149. msg/storage/session.py +403 -0
  150. msg/storage/sqlite.py +360 -0
  151. msg/storage/topic_event_migration.py +26 -0
  152. msg/storage/valkey_bus.py +45 -0
  153. msg/transports/__init__.py +0 -0
  154. msg/transports/client.py +183 -0
  155. msg/transports/dictionary.py +387 -0
  156. msg/transports/graphql.py +75 -0
  157. msg/transports/http.py +72 -0
  158. msg/transports/http_routes.py +1577 -0
  159. msg/transports/mcp.py +127 -0
  160. msg/transports/packet.py +42 -0
  161. msg/transports/read_tree_path.py +65 -0
  162. msg/transports/stdio.py +36 -0
  163. msg/transports/url_safety.py +123 -0
  164. msg/tui.py +368 -0
  165. msg/workers/__init__.py +1 -0
  166. msg/workers/effects.py +406 -0
  167. msg/workers/leases.py +19 -0
  168. msg/workers/mail.py +63 -0
  169. msg/workers/maintenance.py +310 -0
  170. msg/workers/sandbox.py +74 -0
  171. msg/workers/sandbox_child.py +189 -0
  172. msg/workers/webhook.py +160 -0
  173. msgctl-0.1.0a1.dist-info/METADATA +96 -0
  174. msgctl-0.1.0a1.dist-info/RECORD +176 -0
  175. msgctl-0.1.0a1.dist-info/WHEEL +4 -0
  176. msgctl-0.1.0a1.dist-info/entry_points.txt +4 -0
msg/constants.py ADDED
@@ -0,0 +1,11 @@
1
+ """Protocol identities and protected bootstrap IDs, not application workflows."""
2
+ ROOT_SPACE='r_root'
3
+ ROOT_SUBJECT='u_root'
4
+ ONLINE_CA='u_online_ca'
5
+ PUBLIC_GROUP='g_public'
6
+ ADMINS_GROUP='g_admins'
7
+ CA_SPACE='t_ca'
8
+ CSR_SPACE='t_csr'
9
+ CERT_SPACE='t_certificates'
10
+ TOOLS_SPACE='t_tools'
11
+ PROTOCOL_VERSION=1
msg/core/__init__.py ADDED
File without changes
msg/core/batching.py ADDED
@@ -0,0 +1,23 @@
1
+ """The one child-envelope validator for atomic and independent batches."""
2
+ from msg.core.codec import canonical
3
+ from msg.core.errors import require
4
+ from msg.core.packet import decode_packet
5
+ from msg.core.requests import SECRET_DELIVERY_MIN_VERSION
6
+
7
+ # Credential delivery has response-only secrets; it requires its own call.
8
+ NO_BATCH=frozenset(SECRET_DELIVERY_MIN_VERSION) | {'identity.register','identity.upgrade'}
9
+
10
+
11
+ def packets(registry,request,subject,max_bytes=None):
12
+ max_bytes=max_bytes or len(canonical(request))
13
+ values=[]
14
+ for value in request.arguments['requests']:
15
+ packet=decode_packet(value,max_bytes)
16
+ spec=registry.operation(packet.operation,packet.contract_version)
17
+ require(spec.effect in {'read','transaction'} and not packet.operation.startswith('batch.')
18
+ and packet.operation not in NO_BATCH and 'network' in spec.entries,'operation_not_batchable')
19
+ require(packet.subject==subject,'batch_subject_mismatch')
20
+ values.append(packet)
21
+ require(len({p.request_id for p in values})==len(values),'duplicate_batch_request_id')
22
+ require(request.request_id not in {p.request_id for p in values},'recursive_request_id')
23
+ return values
msg/core/codec.py ADDED
@@ -0,0 +1,253 @@
1
+ """Versioned deterministic JSON, strict parsing and immutable contract records.
2
+
3
+ Canonical v1 uses UTF-8, sorted Unicode keys, compact separators and JSON finite
4
+ numbers as emitted by Python. It is not advertised as RFC 8785/JCS. Signed
5
+ request test vectors define the wire representation for non-Python clients.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import dataclasses
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import re
15
+ import types
16
+ import typing
17
+ from functools import lru_cache
18
+ from collections.abc import Mapping
19
+ from datetime import UTC, datetime
20
+ from pathlib import Path
21
+ from types import MappingProxyType
22
+ from .errors import Failure, require
23
+
24
+
25
+ def freeze_json(value):
26
+ if value is None or type(value) in (bool, int, str):
27
+ return value
28
+ if type(value) is float:
29
+ require(math.isfinite(value), "non_finite_float")
30
+ return value
31
+ if isinstance(value, (tuple, list)):
32
+ return tuple(freeze_json(x) for x in value)
33
+ if isinstance(value, Mapping):
34
+ require(all(type(k) is str for k in value), "non_string_json_key")
35
+ return MappingProxyType({k: freeze_json(v) for k, v in value.items()})
36
+ raise Failure("unsupported_json_type")
37
+
38
+
39
+ def b64(data: bytes) -> str:
40
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
41
+
42
+
43
+ def unb64(value: str, *, limit: int = 16 * 1024 * 1024) -> bytes:
44
+ require(type(value) is str and len(value) <= (limit * 4 // 3 + 4), "invalid_base64")
45
+ require(bool(re.fullmatch(r"[A-Za-z0-9_-]*", value)), "invalid_base64")
46
+ try:
47
+ result = base64.b64decode(value + "=" * (-len(value) % 4), altchars=b"-_", validate=True)
48
+ except (ValueError, TypeError) as exc:
49
+ raise Failure("invalid_base64") from exc
50
+ require(b64(result) == value and len(result) <= limit, "invalid_base64")
51
+ return result
52
+
53
+
54
+ def timestamp(value: datetime) -> str:
55
+ require(isinstance(value, datetime) and value.tzinfo is not None and
56
+ value.utcoffset() is not None, "naive_datetime")
57
+ return value.astimezone(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z")
58
+
59
+
60
+ def parse_time(value: str) -> datetime:
61
+ require(type(value) is str and value.endswith("Z"), "invalid_datetime")
62
+ try:
63
+ result = datetime.fromisoformat(value)
64
+ except ValueError as exc:
65
+ raise Failure("invalid_datetime") from exc
66
+ require(result.tzinfo is not None, "naive_datetime")
67
+ return result.astimezone(UTC)
68
+
69
+
70
+ def wire(value, *, compact: bool = False):
71
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
72
+ result = {}
73
+ for field in dataclasses.fields(value):
74
+ v = getattr(value, field.name)
75
+ if v is None and field.metadata.get('omit_if_none'):
76
+ continue
77
+ if compact and (v is None or v == () or v == frozenset()):
78
+ continue
79
+ result[field.name] = f"{v:04o}" if field.name == "mode" else wire(v, compact=compact)
80
+ return result
81
+ if isinstance(value, Mapping):
82
+ return {k: wire(v, compact=compact) for k, v in value.items()
83
+ if not compact or v is not None}
84
+ if isinstance(value, (tuple, list)):
85
+ return [wire(v, compact=compact) for v in value]
86
+ if isinstance(value, (set, frozenset)):
87
+ return sorted((wire(v, compact=compact) for v in value), key=canonical)
88
+ if isinstance(value, datetime):
89
+ return timestamp(value)
90
+ if isinstance(value, bytes):
91
+ return b64(value)
92
+ if isinstance(value, Path):
93
+ return str(value)
94
+ if value is None or type(value) in (bool, int, str):
95
+ return value
96
+ if type(value) is float and math.isfinite(value):
97
+ return value
98
+ raise Failure("invalid_json_value")
99
+
100
+
101
+ def canonical(value) -> bytes:
102
+ try:
103
+ return json.dumps(wire(value), ensure_ascii=False, sort_keys=True,
104
+ separators=(",", ":"), allow_nan=False).encode("utf-8")
105
+ except (UnicodeError, ValueError, TypeError, RecursionError) as exc:
106
+ if isinstance(exc, Failure):
107
+ raise
108
+ raise Failure("invalid_json_value") from exc
109
+
110
+
111
+ def digest(value: bytes | object) -> str:
112
+ return "sha256:" + hashlib.sha256(value if isinstance(value, bytes) else canonical(value)).hexdigest()
113
+
114
+
115
+ def loads(raw: str | bytes):
116
+ def pairs(items):
117
+ result = {}
118
+ for key, value in items:
119
+ require(key not in result, "duplicate_json_key", key)
120
+ result[key] = value
121
+ return result
122
+ def constant(_):
123
+ raise Failure("non_finite_float")
124
+ try:
125
+ value = json.loads(raw, object_pairs_hook=pairs, parse_constant=constant)
126
+ freeze_json(value)
127
+ return value
128
+ except (UnicodeError, ValueError, TypeError, RecursionError) as exc:
129
+ if isinstance(exc, Failure):
130
+ raise
131
+ raise Failure("invalid_json") from exc
132
+
133
+
134
+ @lru_cache(maxsize=256)
135
+ def _record_shape(cls):
136
+ # Registered Python classes are immutable schema definitions. This cache
137
+ # never contains user identities, authorization decisions, or mutable rows.
138
+ return ({f.name: f for f in dataclasses.fields(cls)}, typing.get_type_hints(cls))
139
+
140
+
141
+ def decode(cls, value, field: str = ""):
142
+ """Decode only declared fields; booleans never masquerade as integers."""
143
+ if isinstance(cls, typing.TypeAliasType):
144
+ if cls.__name__ in {"Json", "JsonMap"}:
145
+ if cls.__name__ == "JsonMap":
146
+ require(isinstance(value, Mapping), "invalid_type", field)
147
+ return freeze_json(value)
148
+ return decode(cls.__value__, value, field)
149
+ if hasattr(cls, "__supertype__"):
150
+ return decode(cls.__supertype__, value, field)
151
+ origin, args = typing.get_origin(cls), typing.get_args(cls)
152
+ if origin in (typing.Union, types.UnionType):
153
+ for option in args:
154
+ try:
155
+ return decode(option, value, field)
156
+ except (Failure, TypeError, ValueError):
157
+ continue
158
+ raise Failure("invalid_union", field)
159
+ if origin is typing.Literal:
160
+ require(any(type(value) is type(v) and value == v for v in args), "invalid_enum", field)
161
+ return value
162
+ if origin in (tuple, list, frozenset, set):
163
+ require(isinstance(value, (tuple, list, set, frozenset)), "invalid_type", field)
164
+ if origin is tuple and args and args[-1] is not Ellipsis:
165
+ require(len(args) == len(value), "invalid_length", field)
166
+ return tuple(decode(t, x, field) for t, x in zip(args, value, strict=True))
167
+ child = args[0] if args else typing.Any
168
+ return origin(decode(child, x, field) for x in value)
169
+ if origin in (dict, Mapping):
170
+ require(isinstance(value, Mapping), "invalid_type", field)
171
+ return MappingProxyType({decode(args[0], k, field): decode(args[1], v, field)
172
+ for k, v in value.items()})
173
+ if cls is datetime:
174
+ return parse_time(value)
175
+ if cls is bytes:
176
+ return unb64(value)
177
+ if cls is Path:
178
+ require(type(value) is str, "invalid_type", field)
179
+ return Path(value)
180
+ if cls is typing.Any:
181
+ return freeze_json(value)
182
+ if dataclasses.is_dataclass(cls):
183
+ require(isinstance(value, Mapping), "invalid_type", field)
184
+ fields, hints = _record_shape(cls)
185
+ unknown = set(value) - fields.keys()
186
+ require(not unknown, "unknown_field", (field + "." if field else "") +
187
+ (sorted(unknown)[0] if unknown else ""))
188
+ result = {}
189
+ for name, f in fields.items():
190
+ where = f"{field}.{name}" if field else name
191
+ if name not in value:
192
+ require(f.default is not dataclasses.MISSING or
193
+ f.default_factory is not dataclasses.MISSING, "missing_field", where)
194
+ continue
195
+ if name == "mode":
196
+ mode = value[name]
197
+ require(type(mode) is str and re.fullmatch(r"[0-7]{4}", mode) is not None,
198
+ "invalid_mode", where)
199
+ result[name] = int(mode, 8)
200
+ else:
201
+ result[name] = decode(hints[name], value[name], where)
202
+ return cls(**result)
203
+ if cls in (str, bool, int, float, type(None)):
204
+ require(type(value) is cls, "invalid_type", field)
205
+ if cls is float:
206
+ require(math.isfinite(value), "non_finite_float", field)
207
+ return value
208
+ raise Failure("unsupported_contract_type", field)
209
+
210
+
211
+ def validate_record(obj):
212
+ for f in dataclasses.fields(obj):
213
+ value = getattr(obj, f.name)
214
+ if isinstance(value, datetime):
215
+ require(value.tzinfo is not None and value.utcoffset() is not None, "naive_datetime", f.name)
216
+ object.__setattr__(obj, f.name, value.astimezone(UTC))
217
+ elif isinstance(value, Mapping):
218
+ object.__setattr__(obj, f.name, freeze_json(value))
219
+ elif isinstance(value, list):
220
+ object.__setattr__(obj, f.name, tuple(value))
221
+ elif isinstance(value, set):
222
+ object.__setattr__(obj, f.name, frozenset(value))
223
+ if f.name in {"generation", "size", "expected_size", "offset", "attempts", "delegation_depth",
224
+ "max_child_ca_depth", "max_delegation_depth", "max_redirects"} and value is not None:
225
+ require(type(value) is int and value >= 0, "invalid_nonnegative_integer", f.name)
226
+ if f.name in {"version", "type_version", "format_version", "protocol_version", "contract_version",
227
+ "max_cert_ttl_seconds", "requested_ttl_seconds"}:
228
+ if f.name == "version" and type(obj).__name__ == "PluginManifest":
229
+ require(type(value) is str and bool(value), "invalid_version", f.name)
230
+ elif f.name == "version" and type(obj).__name__ == "Membership":
231
+ require(type(value) is int and value >= 0, "invalid_version", f.name)
232
+ else:
233
+ require(type(value) is int and value >= 1, "invalid_version", f.name)
234
+ if type(obj).__name__ == "Resource":
235
+ from msg.core.tags import normalize_tags
236
+
237
+ require(type(obj.mode) is int and 0 <= obj.mode <= 0o7777, "invalid_mode")
238
+ require(obj.state in {"active", "archived", "purged"}, "invalid_state")
239
+ require(obj.parent != obj.id, "parent_cycle")
240
+ require(obj.tags == normalize_tags(obj.tags), "invalid_tags")
241
+ if type(obj).__name__ == "Relation" and obj.excerpt is not None:
242
+ require(len(obj.excerpt) == 2 and all(type(n) is int for n in obj.excerpt)
243
+ and 0 <= obj.excerpt[0] <= obj.excerpt[1], "invalid_byte_range")
244
+ if hasattr(obj, "not_before") and getattr(obj, "expires_at", None) is not None:
245
+ require(obj.not_before < obj.expires_at, "invalid_validity")
246
+
247
+
248
+ def record(**kwargs):
249
+ """Dataclass decoration plus local validation, not a domain base class."""
250
+ def decorate(cls):
251
+ cls.__post_init__ = validate_record
252
+ return dataclasses.dataclass(**kwargs)(cls)
253
+ return decorate
msg/core/contracts.py ADDED
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from .models import *
4
+ from .query import QuerySession
5
+ from .tool_execution import ToolRunner
6
+
7
+ class MetadataSession(QuerySession, Protocol):
8
+ def on_rollback(self, effect: Callable[[], Awaitable[None]]) -> None: ...
9
+ async def resource(self, id: ResourceId) -> Resource: ...
10
+ async def resolve(self, path: str) -> ResourceId: ...
11
+ async def path(self, id: ResourceId) -> str: ...
12
+ async def ancestors(self, id: ResourceId) -> tuple[Resource, ...]: ...
13
+ async def children(
14
+ self, parent: ResourceId, cursor: str | None, limit: int
15
+ ) -> Page[Resource]: ...
16
+ async def revision(self, ref: ResourceRef) -> Revision: ...
17
+ async def history(
18
+ self, id: ResourceId, cursor: str | None, limit: int
19
+ ) -> Page[Revision]: ...
20
+ async def insert(self, resource: Resource) -> None: ...
21
+ async def replace(
22
+ self, resource: Resource, expected_generation: int
23
+ ) -> None: ...
24
+ async def append_revision(self, revision: Revision) -> None: ...
25
+ async def subject(self, id: ResourceId) -> Subject: ...
26
+ async def organization(self, id: ResourceId) -> Organization: ...
27
+ async def credential(self, id: ID) -> Credential: ...
28
+ async def certificate(self, id: ResourceId) -> Certificate: ...
29
+ async def certificate_revoked(self, id: ResourceId) -> bool: ...
30
+ async def memberships(
31
+ self, subject: ResourceId
32
+ ) -> tuple[Membership, ...]: ...
33
+ async def request_result(
34
+ self, subject: ResourceId, id: RequestId, digest: Digest
35
+ ) -> OperationResult | None: ...
36
+ async def save_result(
37
+ self, subject: ResourceId, digest: Digest, result: OperationResult
38
+ ) -> None: ...
39
+ async def update_identity(
40
+ self, record: Subject | Organization | Membership | EmailSettings,
41
+ expected_generation: int
42
+ ) -> None: ...
43
+ async def save_credential(
44
+ self, credential: Credential, expected_auth_version: int
45
+ ) -> None: ...
46
+ async def csr(self, id: ResourceId) -> CertificateRequest: ...
47
+ async def csr_state(
48
+ self, id: ResourceId
49
+ ) -> CertificateRequestState: ...
50
+ async def save_csr(
51
+ self, request: CertificateRequest
52
+ ) -> None: ...
53
+ async def transition_csr(
54
+ self, state: CertificateRequestState, expected_generation: int
55
+ ) -> None: ...
56
+ async def revoke_certificate(
57
+ self, id: ResourceId, event: AuditEvent
58
+ ) -> None: ...
59
+ async def register_certificate(
60
+ self, certificate: Certificate, csr_id: ResourceId,
61
+ expected_generation: int
62
+ ) -> None: ...
63
+ async def transfer(self, id: ID) -> TransferSession: ...
64
+ async def save_transfer(
65
+ self, session: TransferSession, expected_generation: int | None
66
+ ) -> None: ...
67
+ async def put_chunk(self, chunk: TransferChunk) -> None: ...
68
+ async def missing_ranges(
69
+ self, id: ID, cursor: str | None, limit: int
70
+ ) -> Page[ByteRange]: ...
71
+ async def append_event(self, event: Event) -> None: ...
72
+ async def append_audit(self, event: AuditEvent) -> None: ...
73
+ async def enqueue(self, job: EffectJob) -> None: ...
74
+ async def job(self, id: ID) -> EffectJob: ...
75
+ async def save_job(self, job: EffectJob) -> None: ...
76
+
77
+
78
+ class MetadataStore(Protocol):
79
+ def transaction(
80
+ self, *, write: bool
81
+ ) -> AbstractAsyncContextManager[MetadataSession]: ...
82
+
83
+
84
+ class ContentStore(Protocol):
85
+ async def put(
86
+ self, chunks: AsyncIterator[bytes], media_type: str,
87
+ expected_digest: Digest | None = None
88
+ ) -> BlobRef: ...
89
+ def read(
90
+ self, blob: BlobRef, byte_range: ByteRange | None = None
91
+ ) -> AsyncIterator[bytes]: ...
92
+ async def pin(self, blob: BlobRef, lease_id: ID) -> None: ...
93
+ async def unpin(self, blob: BlobRef, lease_id: ID) -> None: ...
94
+
95
+
96
+ class Authenticator(Protocol):
97
+ async def authenticate(
98
+ self, request: OperationRequest, session: MetadataSession,
99
+ *, entry: Entry
100
+ ) -> Principal: ...
101
+
102
+
103
+ class Authorizer(Protocol):
104
+ async def require(
105
+ self, context: ExecutionContext, request: OperationRequest,
106
+ checks: tuple[AccessRequirement, ...], session: MetadataSession
107
+ ) -> tuple[ResourceRef, ...]: ...
108
+
109
+
110
+ class Signer(Protocol):
111
+ @property
112
+ def key_id(self) -> ID: ...
113
+ def sign(self, payload: bytes, *, purpose: str) -> Signature: ...
114
+
115
+
116
+ class ClientTransport(Protocol):
117
+ async def discover(self) -> TransportLimits: ...
118
+ async def call(self, request: OperationRequest) -> OperationResult: ...
119
+
120
+
121
+ # Compatibility import only. The unused invoke -> ResourceRef declaration never
122
+ # described the injected runner. ToolRunner is now the one actual execution port;
123
+ # publication and current-authority/attempt checks remain owned by EffectWorker.
124
+ ToolExecutor = ToolRunner
125
+
126
+
127
+ class NotificationSender(Protocol):
128
+ async def send(self, job: EffectJob) -> Literal['sent', 'uncertain']: ...
msg/core/cursors.py ADDED
@@ -0,0 +1,68 @@
1
+ """Opaque position cursors are not authentication credentials."""
2
+ import hmac
3
+ from msg.core.codec import b64,unb64,canonical,loads,digest,parse_time,wire
4
+ from msg.core.errors import require
5
+
6
+
7
+ class CursorCodec:
8
+ def __init__(self,key):
9
+ self.key=key
10
+
11
+ def encode(self,kind,query,position):
12
+ data=canonical({'v':1,'kind':kind,'query':query,'position':position})
13
+ tag=hmac.digest(self.key,b'cursor-v1\0'+data,'sha256')
14
+ return b64(data)+'.'+b64(tag)
15
+
16
+ def inspect(self,value):
17
+ require(isinstance(value,str) and len(value)<=8192 and value.count('.')==1,'invalid_cursor')
18
+ encoded,signature=value.split('.')
19
+ raw=unb64(encoded)
20
+ require(hmac.compare_digest(hmac.digest(self.key,b'cursor-v1\0'+raw,'sha256'),unb64(signature)),'invalid_cursor')
21
+ data=loads(raw)
22
+ require(data.get('v')==1,'invalid_cursor')
23
+ return data
24
+
25
+ def decode(self,value,kind,query):
26
+ data=self.inspect(value)
27
+ require(data.get('v')==1 and data.get('kind')==kind and data.get('query')==query,'cursor_query_mismatch')
28
+ return data['position']
29
+
30
+ def encode_page(self,operation,arguments,position,snapshot,principal,expires_at):
31
+ query={'operation':operation,'arguments':arguments}
32
+ return self.encode('read-page',query,{'last':position,'snapshot':wire(snapshot),
33
+ 'principal':principal,'expires_at':wire(expires_at),'query_digest':digest(query)})
34
+
35
+ def inspect_page(self,value,now):
36
+ data=self.inspect(value)
37
+ require(data.get('kind')=='read-page','cursor_kind_mismatch')
38
+ position=data.get('position')
39
+ require(isinstance(position,dict) and position.get('query_digest')==digest(data.get('query')),
40
+ 'invalid_cursor')
41
+ require(now<parse_time(position['expires_at']),'cursor_expired')
42
+ return data['query'],position
43
+
44
+ def decode_page(self,value,operation,arguments,principal,now):
45
+ query,position=self.inspect_page(value,now)
46
+ require(query=={'operation':operation,'arguments':arguments},'cursor_query_mismatch')
47
+ require(position['principal']==principal,'cursor_principal_mismatch')
48
+ return position['last'],parse_time(position['snapshot'])
49
+
50
+ def encode_read(self,ref,max_bytes,offset,fence,line_start,continued,principal,expires_at,
51
+ previous=None):
52
+ query={'ref':wire(ref),'max_bytes':max_bytes}
53
+ return self.encode('read-segment',query,{'offset':offset,'fence':fence,'line_start':line_start,
54
+ 'continued':continued,'principal':principal,'expires_at':wire(expires_at),
55
+ 'previous':previous})
56
+
57
+ def inspect_read(self,value,now):
58
+ data=self.inspect(value)
59
+ require(data.get('kind')=='read-segment','cursor_kind_mismatch')
60
+ position=data.get('position')
61
+ require(isinstance(position,dict) and now<parse_time(position['expires_at']),
62
+ 'cursor_expired')
63
+ return data['query'],position
64
+
65
+ def decode_read(self,value,principal,now):
66
+ query,position=self.inspect_read(value,now)
67
+ require(position['principal']==principal,'cursor_principal_mismatch')
68
+ return query,position
@@ -0,0 +1,33 @@
1
+ """Validate one exact mailbox before binding or passing it to SMTP.
2
+
3
+ An email endpoint is an addr-spec, not a display name, group or address list.
4
+ Do not normalize it: the verified endpoint snapshot must identify the same bytes.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from email import policy
9
+ from email.errors import HeaderParseError, NonASCIILocalPartDefect
10
+ from email.headerregistry import Address
11
+ import re
12
+
13
+ from msg.core.errors import Failure, require
14
+
15
+
16
+ def validate_address(address: str) -> Address:
17
+ require(isinstance(address, str) and len(address) <= 254 and
18
+ re.fullmatch(r'[^\s@]+@[^\s@]+\.[^\s@]+', address) is not None and
19
+ not any(ord(c) < 32 or ord(c) == 127 for c in address), 'invalid_email')
20
+ try:
21
+ header = policy.default.header_factory('To', address)
22
+ mailboxes, groups = header.addresses, header.groups
23
+ except (HeaderParseError, ValueError, IndexError):
24
+ raise Failure('invalid_email') from None
25
+ # SMTPUTF8 mailboxes remain supported. All other parser defects, including
26
+ # obsolete or partially recovered syntax, fail closed rather than redirect.
27
+ require(len(mailboxes) == len(groups) == 1 and groups[0].display_name is None and
28
+ all(isinstance(d, NonASCIILocalPartDefect) for d in header.defects),
29
+ 'invalid_email')
30
+ mailbox = mailboxes[0]
31
+ require(bool(mailbox.username and mailbox.domain) and mailbox.addr_spec == address,
32
+ 'invalid_email')
33
+ return mailbox
msg/core/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ """Stable errors shared by business operations and every transport."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class Failure(ValueError):
6
+ def __init__(self, code: str, field: str | None = None, *, retryable: bool = False,
7
+ details: dict | None = None):
8
+ super().__init__(code)
9
+ self.code, self.field, self.retryable, self.details = code, field, retryable, details
10
+
11
+ def as_dict(self) -> dict:
12
+ value = {"code": self.code, "retryable": self.retryable}
13
+ if self.field:
14
+ value["field_path"] = self.field
15
+ if self.details:
16
+ value["details"] = self.details
17
+ return value
18
+
19
+
20
+ def require(condition: bool, code: str, field: str | None = None, **kwargs) -> None:
21
+ if not condition:
22
+ raise Failure(code, field, **kwargs)
msg/core/events.py ADDED
@@ -0,0 +1,6 @@
1
+ """Deterministic committed-operation event identity shared by all producers."""
2
+ from msg.core.codec import digest
3
+
4
+
5
+ def event_id(request, subject):
6
+ return 'e_' + digest((subject, request.request_id))[7:39]
@@ -0,0 +1,20 @@
1
+ """Fixed executor dependencies, not plugin hooks or a workflow interface.
2
+
3
+ ResultProjection is a permission-checked read in the existing transaction.
4
+ TransactionalEventNotifications may enqueue durable jobs in that transaction;
5
+ it must never perform external I/O or commit it. Execution, auth and receipt
6
+ ownership remain in OperationExecutor.
7
+ """
8
+ from typing import Protocol
9
+ from msg.core.contracts import MetadataSession
10
+ from msg.core.models import Event, ExecutionContext, JsonMap, OperationRequest, ResourceRef
11
+
12
+
13
+ class ResultProjection(Protocol):
14
+ async def __call__(self, context: ExecutionContext, request: OperationRequest,
15
+ session: MetadataSession, resource: ResourceRef,
16
+ *, fields: tuple[str, ...]) -> JsonMap: ...
17
+
18
+
19
+ class TransactionalEventNotifications(Protocol):
20
+ async def __call__(self, session: MetadataSession, event: Event) -> None: ...