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/core/models.py ADDED
@@ -0,0 +1,517 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
5
+ from contextlib import AbstractAsyncContextManager
6
+ from dataclasses import field
7
+ from .codec import record as dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from types import MappingProxyType
11
+ from typing import Literal, NewType, Protocol
12
+
13
+
14
+ ResourceId = NewType('ResourceId', str)
15
+ RevisionId = NewType('RevisionId', str)
16
+ RequestId = NewType('RequestId', str)
17
+ type ID = str
18
+ type Digest = str
19
+ type Json = (
20
+ None | bool | int | float | str | tuple[Json, ...]
21
+ | Mapping[str, Json]
22
+ )
23
+ type JsonMap = Mapping[str, Json]
24
+ type Entry = Literal['network', 'local_admin', 'worker']
25
+ type ResourceState = Literal['active', 'archived', 'purged']
26
+ type ByteRange = tuple[int, int] # [start, end),单位为字节
27
+
28
+
29
+ @dataclass(frozen=True, slots=True, kw_only=True)
30
+ class ResourceRef:
31
+ id: ResourceId
32
+ revision: RevisionId | None = None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True, kw_only=True)
36
+ class BlobRef:
37
+ digest: Digest
38
+ size: int
39
+ media_type: str
40
+
41
+
42
+ @dataclass(frozen=True, slots=True, kw_only=True)
43
+ class Resource:
44
+ id: ResourceId
45
+ type: str
46
+ type_version: int
47
+ name: str
48
+ parent: ResourceId | None
49
+ owner: ResourceId
50
+ group: ResourceId
51
+ mode: int
52
+ generation: int
53
+ revision: RevisionId | None
54
+ state: ResourceState
55
+ created_at: datetime
56
+ created_by: ResourceId
57
+ modified_at: datetime
58
+ modified_by: ResourceId
59
+ tags: tuple[str, ...] = ()
60
+
61
+
62
+ @dataclass(frozen=True, slots=True, kw_only=True)
63
+ class Relation:
64
+ type: str
65
+ target: ResourceRef
66
+ excerpt: ByteRange | None = None
67
+
68
+
69
+ @dataclass(frozen=True, slots=True, kw_only=True)
70
+ class Signature:
71
+ key_id: ID
72
+ algorithm: str
73
+ value: bytes = field(repr=False)
74
+
75
+
76
+ @dataclass(frozen=True, slots=True, kw_only=True)
77
+ class Revision:
78
+ format_version: int
79
+ id: RevisionId
80
+ resource_id: ResourceId
81
+ parents: tuple[RevisionId, ...]
82
+ content: BlobRef
83
+ relations: tuple[Relation, ...]
84
+ actor: ResourceId
85
+ subject: ResourceId
86
+ author: ResourceId
87
+ created_at: datetime
88
+ manifest_digest: Digest
89
+ signature: Signature | None = None
90
+ change_note: str | None = field(default=None, metadata={'omit_if_none': True})
91
+ source_kind: Literal['release','user','operation'] | None = field(
92
+ default=None, metadata={'omit_if_none': True})
93
+ source_version: int | None = field(default=None, metadata={'omit_if_none': True})
94
+ source_digest: Digest | None = field(default=None, metadata={'omit_if_none': True})
95
+ signature_source: Literal['custodial'] | None = field(default=None,metadata={'omit_if_none':True})
96
+
97
+
98
+ @dataclass(frozen=True, slots=True, kw_only=True)
99
+ class Page[T]:
100
+ items: tuple[T, ...]
101
+ next_cursor: str | None = None
102
+ @dataclass(frozen=True, slots=True, kw_only=True)
103
+ class Subject:
104
+ resource_id: ResourceId
105
+ kind: Literal['temporary', 'registered', 'custodial', 'system']
106
+ primary_group: ResourceId
107
+ auth_version: int
108
+ local_only: bool = False
109
+
110
+
111
+ @dataclass(frozen=True, slots=True, kw_only=True)
112
+ class Organization:
113
+ resource_id: ResourceId
114
+ membership_version: int
115
+ builtin: Literal['public', 'admins'] | None = None
116
+ membership_policy: Literal['open', 'approval', 'invite', 'managed'] = 'invite'
117
+
118
+
119
+ @dataclass(frozen=True, slots=True, kw_only=True)
120
+ class Membership:
121
+ organization_id: ResourceId
122
+ subject_id: ResourceId
123
+ role: Literal['owner', 'maintainer', 'member', 'admin']
124
+ version: int
125
+ status: Literal['active', 'pending', 'invited', 'rejected'] = 'active'
126
+ joined_at: datetime | None = None
127
+ invited_by: ResourceId | None = None
128
+
129
+
130
+ @dataclass(frozen=True, slots=True, kw_only=True)
131
+ class EmailSettings:
132
+ subject_id: ResourceId
133
+ address: str | None = field(repr=False)
134
+ verified_at: datetime | None
135
+ enabled_events: frozenset[str] = frozenset()
136
+
137
+
138
+ @dataclass(frozen=True, slots=True, kw_only=True)
139
+ class Scope:
140
+ resource_id: ResourceId
141
+ descendants: bool = False
142
+
143
+
144
+ @dataclass(frozen=True, slots=True, kw_only=True)
145
+ class CapabilityGrant:
146
+ capability: str
147
+ version: int
148
+ scope: Scope
149
+ operations: frozenset[str]
150
+ constraints: JsonMap
151
+
152
+
153
+ @dataclass(frozen=True, slots=True, kw_only=True)
154
+ class IssuancePolicy:
155
+ issue_grants: tuple[CapabilityGrant, ...]
156
+ max_cert_ttl_seconds: int
157
+ max_child_ca_depth: int
158
+ max_delegation_depth: int
159
+
160
+
161
+ @dataclass(frozen=True, slots=True, kw_only=True)
162
+ class Credential:
163
+ id: ID
164
+ subject_id: ResourceId
165
+ kind: Literal['signing_key', 'ssh_key', 'token']
166
+ verifier: bytes = field(repr=False)
167
+ ceiling: tuple[CapabilityGrant, ...]
168
+ not_before: datetime
169
+ expires_at: datetime | None
170
+ revoked_at: datetime | None
171
+
172
+
173
+ @dataclass(frozen=True, slots=True, kw_only=True)
174
+ class Certificate:
175
+ resource_id: ResourceId
176
+ serial: str
177
+ subject_id: ResourceId
178
+ key_id: ID
179
+ issuer_id: ResourceId
180
+ parent_certificate_id: ResourceId | None
181
+ authority_sources: tuple[ResourceRef, ...]
182
+ kind: Literal['identity', 'delegation', 'capability', 'ca']
183
+ grants: tuple[CapabilityGrant, ...]
184
+ not_before: datetime
185
+ expires_at: datetime
186
+ target_service: str
187
+ delegation_depth: int
188
+ issuance: IssuancePolicy | None
189
+ signature: Signature
190
+
191
+
192
+ @dataclass(frozen=True, slots=True, kw_only=True)
193
+ class CertificateRequest:
194
+ resource_id: ResourceId # csr_id 对应的资源
195
+ applicant: ResourceId
196
+ subject_id: ResourceId
197
+ requested_issuer: ResourceId
198
+ public_key: bytes
199
+ kind: Literal['identity', 'delegation', 'capability', 'ca']
200
+ grants: tuple[CapabilityGrant, ...]
201
+ issuance: IssuancePolicy | None
202
+ requested_ttl_seconds: int
203
+ target_service: str
204
+ delegation_depth: int
205
+ authority_sources: tuple[ResourceRef, ...]
206
+ request_digest: Digest
207
+ possession_proof: Signature
208
+
209
+
210
+ @dataclass(frozen=True, slots=True, kw_only=True)
211
+ class CertificateRequestState:
212
+ request_id: ResourceId
213
+ status: Literal['pending', 'issued', 'rejected', 'cancelled', 'expired']
214
+ generation: int
215
+ certificate_id: ResourceId | None = None
216
+
217
+
218
+ @dataclass(frozen=True, slots=True, kw_only=True)
219
+ class SignatureProof:
220
+ signature: Signature
221
+ certificates: tuple[ResourceId, ...]
222
+
223
+
224
+ @dataclass(frozen=True, slots=True, kw_only=True)
225
+ class TokenProof:
226
+ credential_id: ID
227
+ token: bytes = field(repr=False)
228
+
229
+
230
+ type RequestProof = SignatureProof | TokenProof | None
231
+
232
+
233
+ @dataclass(frozen=True, slots=True, kw_only=True)
234
+ class Principal:
235
+ actor: ResourceId | None
236
+ subject: ResourceId | None
237
+ credential_id: ID | None
238
+ method: Literal['anonymous', 'signature', 'token', 'ssh', 'local']
239
+ certificates: tuple[ResourceId, ...]
240
+ ceiling: tuple[CapabilityGrant, ...]
241
+ @dataclass(frozen=True, slots=True, kw_only=True)
242
+ class OperationRequest:
243
+ request_id: RequestId
244
+ protocol_version: int
245
+ operation: str
246
+ contract_version: int
247
+ target_service: str
248
+ subject: ResourceId | None
249
+ arguments: JsonMap
250
+ expected_generations: tuple[tuple[ResourceId, int], ...]
251
+ expires_at: datetime | None
252
+ payload_digest: Digest
253
+ proof: RequestProof = field(repr=False)
254
+ return_fields: tuple[str, ...] = ()
255
+ source: Literal['msg', 'manual', 'mcp', 'unknown'] = 'unknown'
256
+
257
+
258
+ @dataclass(frozen=True, slots=True, kw_only=True)
259
+ class OperationError:
260
+ code: str
261
+ retryable: bool
262
+ field_path: str | None = None
263
+ retry_after_seconds: int | None = None
264
+
265
+
266
+ class OperationFailure(Exception):
267
+ def __init__(self, error: OperationError) -> None:
268
+ self.error = error
269
+ super().__init__(error.code)
270
+
271
+
272
+ @dataclass(frozen=True, slots=True, kw_only=True)
273
+ class HandlerOutput:
274
+ resources: tuple[ResourceRef, ...] = ()
275
+ data: JsonMap | None = None
276
+ output: ResourceRef | None = None
277
+
278
+
279
+ @dataclass(frozen=True, slots=True, kw_only=True)
280
+ class OperationResult:
281
+ request_id: RequestId
282
+ operation: str
283
+ status: Literal['ok', 'accepted', 'error', 'uncertain']
284
+ actor: ResourceId | None
285
+ subject: ResourceId | None
286
+ resources: tuple[ResourceRef, ...] = ()
287
+ committed_at: datetime | None = None
288
+ replayed: bool = False
289
+ receipt: Signature | None = None
290
+ error: OperationError | None = None
291
+ data: JsonMap | None = None
292
+ output: ResourceRef | None = None
293
+ prefer_cli: bool = False
294
+ cli_url: str | None = None
295
+
296
+
297
+ @dataclass(frozen=True, slots=True, kw_only=True)
298
+ class ExecutionContext:
299
+ request_id: RequestId
300
+ principal: Principal
301
+ entry: Entry
302
+ now: datetime
303
+ deadline_monotonic: float
304
+
305
+
306
+ @dataclass(frozen=True, slots=True, kw_only=True)
307
+ class AccessRequirement:
308
+ resource_id: ResourceId
309
+ operation: str
310
+ check: Literal[
311
+ 'read', 'write', 'list', 'traverse', 'create', 'remove',
312
+ 'chmod', 'chgrp', 'chown', 'manage', 'certgate', 'tool_use', 'purge'
313
+ ]
314
+
315
+
316
+ @dataclass(frozen=True, slots=True, kw_only=True)
317
+ class ResourceTypeSpec:
318
+ name: str
319
+ version: int
320
+ container: bool
321
+ content_schema: ResourceRef | None
322
+ operations: frozenset[str]
323
+ relations: frozenset[str]
324
+ taggable: bool = False
325
+ purchasable: bool = False
326
+
327
+
328
+ @dataclass(frozen=True, slots=True, kw_only=True)
329
+ class CapabilitySpec:
330
+ name: str
331
+ version: int
332
+ scope_types: frozenset[str]
333
+ operations: frozenset[str]
334
+ replaces_checks: frozenset[str]
335
+ delegatable: bool
336
+ ca_only: bool
337
+ constraints_schema: ResourceRef | None
338
+
339
+
340
+ @dataclass(frozen=True, slots=True, kw_only=True)
341
+ class OperationSpec:
342
+ name: str
343
+ version: int
344
+ input_schema: ResourceRef
345
+ output_schema: ResourceRef
346
+ effect: Literal['read', 'transaction', 'external']
347
+ entries: frozenset[Entry]
348
+ require_signature: bool
349
+ requirements: Callable[
350
+ [OperationRequest, MetadataSession],
351
+ Awaitable[tuple[AccessRequirement, ...]]
352
+ ]
353
+ handler: Callable[
354
+ [ExecutionContext, OperationRequest, MetadataSession],
355
+ Awaitable[HandlerOutput]
356
+ ]
357
+ @dataclass(frozen=True, slots=True, kw_only=True)
358
+ class TransportLimits:
359
+ max_request_bytes: int
360
+ max_response_bytes: int
361
+ max_path_bytes: int | None
362
+ encodings: frozenset[str]
363
+
364
+
365
+ @dataclass(frozen=True, slots=True, kw_only=True)
366
+ class TransferSession:
367
+ id: ID
368
+ subject_id: ResourceId
369
+ direction: Literal['upload', 'download']
370
+ state: Literal['open', 'sealed', 'cancelled', 'expired']
371
+ target: ResourceRef | None
372
+ expected_size: int | None
373
+ expected_digest: Digest | None
374
+ expires_at: datetime
375
+ generation: int
376
+ output: ResourceRef | None = None
377
+
378
+
379
+ @dataclass(frozen=True, slots=True, kw_only=True)
380
+ class TransferChunk:
381
+ transfer_id: ID
382
+ offset: int
383
+ content: BlobRef
384
+
385
+
386
+ @dataclass(frozen=True, slots=True, kw_only=True)
387
+ class Event:
388
+ id: ID
389
+ type: str
390
+ time: datetime
391
+ request_id: RequestId
392
+ actor: ResourceId
393
+ subject: ResourceId
394
+ resources: tuple[ResourceRef, ...]
395
+ data: JsonMap
396
+
397
+
398
+ @dataclass(frozen=True, slots=True, kw_only=True)
399
+ class AuditEvent:
400
+ event: Event
401
+ authority: tuple[ResourceRef, ...]
402
+ before_digest: Digest | None
403
+ after_digest: Digest | None
404
+ previous_digest: Digest | None
405
+ entry_digest: Digest
406
+ result: str
407
+
408
+
409
+ @dataclass(frozen=True, slots=True, kw_only=True)
410
+ class EffectJob:
411
+ id: ID
412
+ event_id: ID
413
+ kind: str
414
+ dedupe_key: str
415
+ principal: Principal
416
+ operation: str
417
+ arguments: JsonMap
418
+ state: Literal['pending', 'running', 'done', 'failed', 'uncertain']
419
+ attempts: int
420
+ next_attempt_at: datetime
421
+ lease_until: datetime | None
422
+ result: ResourceRef | None = None
423
+ @dataclass(frozen=True, slots=True, kw_only=True)
424
+ class FieldSpec:
425
+ name: str
426
+ type: Literal['str', 'text', 'int', 'bool', 'enum', 'ref', 'file']
427
+ required: bool
428
+ default_json: bytes | None = None # None=无默认,b'null'=默认null
429
+ choices: tuple[str, ...] = ()
430
+ constraints: JsonMap = field(
431
+ default_factory=lambda: MappingProxyType({})
432
+ )
433
+
434
+
435
+ @dataclass(frozen=True, slots=True, kw_only=True)
436
+ class TemplateSpec:
437
+ resource: ResourceRef
438
+ digest: Digest
439
+ fields: tuple[FieldSpec, ...]
440
+ renderer_version: int
441
+
442
+
443
+ @dataclass(frozen=True, slots=True, kw_only=True)
444
+ class NetworkPolicy:
445
+ schemes: frozenset[str]
446
+ hosts: tuple[str, ...]
447
+ ports: frozenset[int]
448
+ methods: frozenset[str]
449
+ allow_private: bool
450
+ timeout_ms: int
451
+ max_response_bytes: int
452
+ max_redirects: int
453
+
454
+
455
+ @dataclass(frozen=True, slots=True, kw_only=True)
456
+ class ToolSpec:
457
+ resource: ResourceRef
458
+ operation: str
459
+ input_schema: ResourceRef
460
+ output_schema: ResourceRef
461
+ executor_key: str
462
+ network: NetworkPolicy
463
+
464
+
465
+ @dataclass(frozen=True, slots=True, kw_only=True)
466
+ class PluginManifest:
467
+ name: str
468
+ version: str
469
+ dependencies: tuple[str, ...]
470
+ resource_types: tuple[ResourceTypeSpec, ...]
471
+ capabilities: tuple[CapabilitySpec, ...]
472
+ operations: tuple[OperationSpec, ...]
473
+ migrations: tuple[str, ...]
474
+
475
+
476
+ @dataclass(frozen=True, slots=True, kw_only=True)
477
+ class BootstrapManifest:
478
+ version: int
479
+ digest: Digest
480
+ resources: tuple[JsonMap, ...] # 经固定 seed schema 校验
481
+ organizations: tuple[JsonMap, ...]
482
+ templates: tuple[BlobRef, ...]
483
+
484
+
485
+ @dataclass(frozen=True, slots=True, kw_only=True)
486
+ class MailConfig:
487
+ enabled: bool
488
+ host: str
489
+ port: int
490
+ tls: Literal['starttls', 'tls']
491
+ sender: str
492
+ credential_file: Path | None = field(repr=False)
493
+
494
+
495
+ @dataclass(frozen=True, slots=True, kw_only=True)
496
+ class ServerConfig:
497
+ config_dir: Path
498
+ postgres_dsn: str = field(repr=False)
499
+ valkey_url: str | None = field(repr=False)
500
+ content_dir: Path
501
+ repositories_dir: Path
502
+ blob_dir: Path
503
+ staging_dir: Path
504
+ service_keys_dir: Path
505
+ plugins: tuple[str, ...]
506
+ limits: TransportLimits
507
+ mail: MailConfig | None = None
508
+
509
+
510
+ @dataclass(frozen=True, slots=True, kw_only=True)
511
+ class ClientConfig:
512
+ config_dir: Path
513
+ server_url: str
514
+ subject_id: ResourceId | None
515
+ credential_file: Path | None = field(repr=False)
516
+ transport: str
517
+ limits: TransportLimits
msg/core/packet.py ADDED
@@ -0,0 +1,71 @@
1
+ """Transport-neutral request/result contracts and bounded canonical decoding."""
2
+ from __future__ import annotations
3
+ from collections.abc import Mapping
4
+ import re
5
+ from msg.core.codec import decode, loads, canonical, wire
6
+ from msg.core.errors import require
7
+ from msg.core.models import OperationRequest, OperationResult
8
+ from msg.core.schemas import obj, IDENTIFIER, STRING, BYTES, REF, SIGNATURE
9
+
10
+ REQUEST_SCHEMA=obj({
11
+ 'request_id':{'type':'string','minLength':1,'maxLength':128},'protocol_version':{'const':1},
12
+ 'operation':{'type':'string','pattern':'^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$','maxLength':128},
13
+ 'contract_version':{'type':'integer','minimum':1},'target_service':{'type':'string','maxLength':2048},
14
+ 'subject':{'type':['string','null'],'maxLength':160},'arguments':{'type':'object'},
15
+ 'expected_generations':{'type':'array','maxItems':64,'items':{'type':'array','prefixItems':[IDENTIFIER,{'type':'integer','minimum':0}],
16
+ 'minItems':2,'maxItems':2}},
17
+ 'expires_at':{'type':['string','null'],'maxLength':40},
18
+ 'payload_digest':{'type':'string','pattern':'^sha256:[0-9a-f]{64}$'},
19
+ 'proof':{'anyOf':[{'type':'null'},obj({'signature':SIGNATURE,'certificates':{'type':'array','items':IDENTIFIER,'maxItems':16}},('signature','certificates')),
20
+ obj({'credential_id':IDENTIFIER,'token':BYTES},('credential_id','token'))]},
21
+ 'return_fields':{'type':'array','maxItems':30,'items':STRING},'source':{'enum':['msg','manual','mcp','unknown']}
22
+ },('request_id','protocol_version','operation','contract_version','target_service','subject','arguments',
23
+ 'expected_generations','expires_at','payload_digest','proof'))
24
+ RESULT_SCHEMA={'type':'object','properties':{
25
+ 'request_id':STRING,'operation':STRING,'status':{'enum':['ok','accepted','error','uncertain']},
26
+ 'actor':STRING,'subject':STRING,'resources':{'type':'array','items':REF},'committed_at':STRING,
27
+ 'replayed':{'type':'boolean'},'receipt':SIGNATURE,'error':{'type':'object'},'data':{'type':'object'},
28
+ 'output':REF,'prefer_cli':{'type':'boolean'},'cli_url':STRING},
29
+ 'required':['request_id','operation','status'],'additionalProperties':False}
30
+
31
+
32
+ def decode_packet(value,max_bytes=1048576):
33
+ from jsonschema import Draft202012Validator
34
+ if isinstance(value,(bytes,str)):
35
+ require(len(value.encode() if isinstance(value,str) else value)<=max_bytes,'request_too_large')
36
+ value=loads(value)
37
+ value=wire(value)
38
+ require(len(canonical(value))<=max_bytes,'request_too_large')
39
+ error=next(Draft202012Validator(REQUEST_SCHEMA).iter_errors(value),None)
40
+ require(error is None,'invalid_request_envelope', '.'.join(map(str,error.path)) if error else None)
41
+ packet=decode(OperationRequest,value)
42
+ require(len(set(rid for rid,generation in packet.expected_generations))==len(packet.expected_generations),
43
+ 'duplicate_expected_generation')
44
+ return packet
45
+
46
+
47
+ def safe_error_code(value, fallback):
48
+ """Only bounded contract identifiers, never exception text or credential URLs."""
49
+ return (value if isinstance(value, str) and
50
+ re.fullmatch(r'[a-z][a-z0-9_]{0,95}', value) else fallback)
51
+
52
+
53
+ def decode_result(value):
54
+ # Reject malformed signed/unsigned results rather than rewriting their error
55
+ # fields. Valid receipts and persisted result bytes are left unchanged.
56
+ require(isinstance(value, Mapping), 'invalid_result_envelope')
57
+ error = value.get('error')
58
+ if error is not None:
59
+ require(isinstance(error, Mapping) and safe_error_code(error.get('code'), None) is not None,
60
+ 'invalid_result_envelope')
61
+ # Compact results omit anonymous actor/subject; these required domain fields
62
+ # are restored explicitly, not inferred from a transport connection.
63
+ return decode(OperationResult,dict({'actor':None,'subject':None},**value))
64
+
65
+
66
+ def result_wire(result):
67
+ value=wire(result,compact=True)
68
+ for name in ('replayed','prefer_cli'):
69
+ if not value.get(name):
70
+ value.pop(name,None)
71
+ return value
@@ -0,0 +1,14 @@
1
+ """Mode helpers; complete authorization lives in security.authorization."""
2
+ from msg.security.policy import (
3
+ CERTGATE,SETGID,STICKY,READ,WRITE,EXECUTE,allows,class_bits,
4
+ format_mode,inherit_group,selected_class,
5
+ )
6
+
7
+ def has_certgate(mode):
8
+ return bool(mode&CERTGATE)
9
+
10
+ def has_setgid(mode):
11
+ return bool(mode&SETGID)
12
+
13
+ def has_sticky(mode):
14
+ return bool(mode&STICKY)
msg/core/query.py ADDED
@@ -0,0 +1,44 @@
1
+ """Explicit SQL query port for trusted server-side domain consumers.
2
+
3
+ Queries use the shared positional '?' subset; a backend may translate it.
4
+ This is not an ORM or an untrusted-SQL interface. Handlers/requirements, local
5
+ administrative use cases and workers may consume it within the transaction
6
+ provided by MetadataStore; adapters/clients must not construct sessions.
7
+ Writes require a write transaction and write=True. Results cannot escape its
8
+ owning task or lifetime. Integrity conflicts are constraint_conflict, and
9
+ PostgreSQL lock/deadlock timeouts are retryable server_busy; adapter-specific
10
+ programming errors still abort the transaction. No commit or connection port
11
+ is exposed. Driver and database protections remain the authority for SQL.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterator, Mapping, Sequence
16
+ from decimal import Decimal
17
+ from typing import Protocol, runtime_checkable
18
+
19
+ # PostgreSQL SUM(bigint) returns Decimal; ordinary stored times/JSON are text.
20
+ type SqlValue = None | bool | int | float | str | bytes | Decimal
21
+ type SqlRow = tuple[SqlValue, ...]
22
+ type SqlParameters = Sequence[SqlValue]
23
+ type SettingValue = (
24
+ None | bool | int | float | str | list[SettingValue]
25
+ | tuple[SettingValue, ...] | Mapping[str, SettingValue]
26
+ )
27
+
28
+
29
+ class QueryResult(Protocol):
30
+ @property
31
+ def rowcount(self) -> int: ...
32
+ def fetchone(self) -> SqlRow | None: ...
33
+ def fetchall(self) -> list[SqlRow]: ...
34
+ def __iter__(self) -> Iterator[SqlRow]: ...
35
+
36
+
37
+ @runtime_checkable
38
+ class QuerySession(Protocol):
39
+ def execute(self, sql: str, parameters: SqlParameters = (), *,
40
+ write: bool = False) -> QueryResult: ...
41
+ def one(self, sql: str, parameters: SqlParameters = ()) -> SqlRow | None: ...
42
+ def rows(self, sql: str, parameters: SqlParameters = ()) -> list[SqlRow]: ...
43
+ def setting(self, key: str, default: SettingValue = None) -> SettingValue: ...
44
+ def set_setting(self, key: str, value: SettingValue) -> None: ...