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.
- msg/__init__.py +2 -0
- msg/admin/__init__.py +0 -0
- msg/admin/backup_retirement.py +56 -0
- msg/admin/backups.py +341 -0
- msg/admin/custodial_check.py +77 -0
- msg/admin/diagnostics.py +750 -0
- msg/admin/market.py +91 -0
- msg/admin/market_check.py +341 -0
- msg/admin/money.py +322 -0
- msg/admin/preflight.py +142 -0
- msg/admin/recovery_replay.py +281 -0
- msg/admin/restore_database.py +37 -0
- msg/admin/root.py +347 -0
- msg/admin/rotation.py +197 -0
- msg/admin/token_delivery_check.py +54 -0
- msg/admin/upgrade_check.py +58 -0
- msg/application.py +163 -0
- msg/bootstrap.py +305 -0
- msg/cli.py +537 -0
- msg/client.py +725 -0
- msg/client_certificates.py +98 -0
- msg/client_content.py +43 -0
- msg/client_custodial.py +206 -0
- msg/client_market.py +104 -0
- msg/client_recovery.py +186 -0
- msg/client_secrets.py +58 -0
- msg/client_tokens.py +99 -0
- msg/client_upgrade.py +188 -0
- msg/config.py +318 -0
- msg/constants.py +11 -0
- msg/core/__init__.py +0 -0
- msg/core/batching.py +23 -0
- msg/core/codec.py +253 -0
- msg/core/contracts.py +128 -0
- msg/core/cursors.py +68 -0
- msg/core/email_address.py +33 -0
- msg/core/errors.py +22 -0
- msg/core/events.py +6 -0
- msg/core/execution_ports.py +20 -0
- msg/core/executor.py +193 -0
- msg/core/models.py +517 -0
- msg/core/packet.py +71 -0
- msg/core/permissions.py +14 -0
- msg/core/query.py +44 -0
- msg/core/read_query.py +60 -0
- msg/core/registry.py +173 -0
- msg/core/requests.py +58 -0
- msg/core/schema_policy.py +28 -0
- msg/core/schemas.py +30 -0
- msg/core/tags.py +21 -0
- msg/core/template_dsl.py +94 -0
- msg/core/text_patch.py +283 -0
- msg/core/tool_execution.py +28 -0
- msg/core/transfer.py +183 -0
- msg/daemon.py +255 -0
- msg/data/__init__.py +0 -0
- msg/data/bootstrap.json +72 -0
- msg/data/favicon.png +0 -0
- msg/data/logo-dark.svg +5 -0
- msg/data/logo.svg +5 -0
- msg/data/recovery-checkpoint.example.json +1 -0
- msg/data/recovery-checkpoint.schema.json +118 -0
- msg/data/shortcodes.json +1 -0
- msg/data/system/AGENTS.md +6 -0
- msg/data/system/rules/_index.md +17 -0
- msg/data/system/rules/auth.md +6 -0
- msg/data/system/rules/files.md +6 -0
- msg/data/system/rules/identity.md +6 -0
- msg/data/system/rules/protocol.md +6 -0
- msg/data/system/rules/read-write.md +6 -0
- msg/data/system/rules/recovery.md +6 -0
- msg/data/system/rules/security.md +10 -0
- msg/data/system/rules/topics.md +6 -0
- msg/extensions/__init__.py +1 -0
- msg/extensions/hosting.py +259 -0
- msg/extensions/keystore.py +96 -0
- msg/extensions/repositories.py +769 -0
- msg/extensions/rss.py +67 -0
- msg/extensions/ssh.py +264 -0
- msg/extensions/ssh_git.py +300 -0
- msg/extensions/tools.py +150 -0
- msg/hosting_runtime.py +138 -0
- msg/market/__init__.py +1 -0
- msg/market/arbitration.py +481 -0
- msg/market/delivery.py +338 -0
- msg/market/delivery_notifications.py +110 -0
- msg/market/delivery_targets.py +76 -0
- msg/market/email.py +100 -0
- msg/market/escrow.py +345 -0
- msg/market/orders.py +218 -0
- msg/market/policy.py +147 -0
- msg/market/rationale.py +91 -0
- msg/market/references.py +19 -0
- msg/market/targets.py +157 -0
- msg/plugins/__init__.py +28 -0
- msg/plugins/achievements.py +316 -0
- msg/plugins/batch.py +32 -0
- msg/plugins/bounty.py +366 -0
- msg/plugins/collaboration.py +261 -0
- msg/plugins/common.py +217 -0
- msg/plugins/communication.py +853 -0
- msg/plugins/content.py +855 -0
- msg/plugins/custodial_lifecycle.py +224 -0
- msg/plugins/delivery.py +230 -0
- msg/plugins/discovery.py +1266 -0
- msg/plugins/discussion.py +162 -0
- msg/plugins/extensions.py +10 -0
- msg/plugins/following.py +78 -0
- msg/plugins/hosting_capacity.py +52 -0
- msg/plugins/identity.py +1654 -0
- msg/plugins/money.py +221 -0
- msg/plugins/offers.py +183 -0
- msg/plugins/orders.py +273 -0
- msg/plugins/recovery.py +376 -0
- msg/plugins/schemas.py +5 -0
- msg/plugins/sharing.py +300 -0
- msg/plugins/store.py +249 -0
- msg/plugins/system.py +75 -0
- msg/plugins/transfer.py +249 -0
- msg/py.typed +0 -0
- msg/security/__init__.py +0 -0
- msg/security/age_keys.py +112 -0
- msg/security/authentication.py +141 -0
- msg/security/authorization.py +300 -0
- msg/security/backup_retirement.py +135 -0
- msg/security/capabilities.py +157 -0
- msg/security/certificates.py +183 -0
- msg/security/crypto.py +102 -0
- msg/security/custodial_migration.py +292 -0
- msg/security/custody_history.py +146 -0
- msg/security/network.py +81 -0
- msg/security/policy.py +67 -0
- msg/security/quarantine.py +19 -0
- msg/security/root_files.py +67 -0
- msg/security/rotation_journal.py +60 -0
- msg/security/sealed_box.py +70 -0
- msg/security/sharing_policy.py +29 -0
- msg/security/token_delivery.py +93 -0
- msg/security/vault.py +186 -0
- msg/storage/__init__.py +0 -0
- msg/storage/capacity.py +100 -0
- msg/storage/custodial_migration.py +22 -0
- msg/storage/git.py +435 -0
- msg/storage/ledger_migration.py +307 -0
- msg/storage/market_migration.py +49 -0
- msg/storage/postgres.py +663 -0
- msg/storage/query.py +39 -0
- msg/storage/read_only.py +126 -0
- msg/storage/session.py +403 -0
- msg/storage/sqlite.py +360 -0
- msg/storage/topic_event_migration.py +26 -0
- msg/storage/valkey_bus.py +45 -0
- msg/transports/__init__.py +0 -0
- msg/transports/client.py +183 -0
- msg/transports/dictionary.py +387 -0
- msg/transports/graphql.py +75 -0
- msg/transports/http.py +72 -0
- msg/transports/http_routes.py +1577 -0
- msg/transports/mcp.py +127 -0
- msg/transports/packet.py +42 -0
- msg/transports/read_tree_path.py +65 -0
- msg/transports/stdio.py +36 -0
- msg/transports/url_safety.py +123 -0
- msg/tui.py +368 -0
- msg/workers/__init__.py +1 -0
- msg/workers/effects.py +406 -0
- msg/workers/leases.py +19 -0
- msg/workers/mail.py +63 -0
- msg/workers/maintenance.py +310 -0
- msg/workers/sandbox.py +74 -0
- msg/workers/sandbox_child.py +189 -0
- msg/workers/webhook.py +160 -0
- msgctl-0.1.0a1.dist-info/METADATA +96 -0
- msgctl-0.1.0a1.dist-info/RECORD +176 -0
- msgctl-0.1.0a1.dist-info/WHEEL +4 -0
- 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
|
msg/core/permissions.py
ADDED
|
@@ -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: ...
|