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/executor.py ADDED
@@ -0,0 +1,193 @@
1
+ """The only network business execution pipeline."""
2
+ from __future__ import annotations
3
+ import logging
4
+ import time
5
+ from dataclasses import replace
6
+ from uuid import uuid4
7
+ from msg.core.codec import wire,digest
8
+ from msg.core.batching import packets
9
+ from msg.core.events import event_id
10
+ from msg.core.packet import result_wire
11
+ from msg.core.execution_ports import ResultProjection, TransactionalEventNotifications
12
+ from msg.core.errors import Failure,require
13
+ from msg.core.models import ExecutionContext,HandlerOutput,OperationResult,OperationError,Event,AccessRequirement
14
+ from msg.core.requests import SECRET_DELIVERY_MIN_VERSION,receipt_bytes
15
+
16
+ log=logging.getLogger(__name__)
17
+
18
+
19
+ class OperationExecutor:
20
+ def __init__(self,registry,metadata,contents,authenticator,authorizer,clock,receipt_signer,
21
+ *, max_request_bytes: int | None = None,
22
+ result_projection: ResultProjection | None = None,
23
+ event_notifications: TransactionalEventNotifications | None = None):
24
+ self.registry,self.metadata,self.contents=registry,metadata,contents
25
+ self.authenticator,self.authorizer,self.clock=authenticator,authorizer,clock
26
+ self.receipt_signer=receipt_signer
27
+ self.response_hook=None
28
+ self.recovery_drill_marker=None
29
+ self.recovery_quarantined=False
30
+ require(max_request_bytes is None or (type(max_request_bytes) is int and max_request_bytes>0),
31
+ 'invalid_request_limit')
32
+ self.max_request_bytes=max_request_bytes
33
+ self.result_projection=result_projection
34
+ self.event_notifications=event_notifications
35
+
36
+ def recovery_drill_active(self):
37
+ marker=self.recovery_drill_marker
38
+ return self.recovery_quarantined or (marker is not None and (marker.exists() or marker.is_symlink()))
39
+
40
+ async def execute(self,request, *, entry='network'):
41
+ principal=None
42
+ try:
43
+ spec=self.registry.operation(request.operation,request.contract_version)
44
+ min_version=SECRET_DELIVERY_MIN_VERSION.get(spec.name)
45
+ if spec.name=='identity.temporary' and spec.version<3:
46
+ raise Failure('temporary_dual_keys_required',
47
+ details={'contract_version':3})
48
+ require(min_version is None or spec.version>=min_version,
49
+ 'credential_delivery_upgrade_required',
50
+ details={'contract_version':min_version} if min_version is not None else None)
51
+ require(entry in spec.entries,'entry_not_allowed')
52
+ self.registry.validate(spec.input_schema,request.arguments)
53
+ if self.recovery_drill_active():
54
+ require(spec.effect=='read','writes_paused')
55
+ raise Failure('recovery_quarantined')
56
+ if spec.name=='batch.independent':
57
+ return await self._independent(request,spec,entry)
58
+ async with self.metadata.transaction(write=spec.effect!='read') as session:
59
+ principal=await self.authenticator.authenticate(request,session,entry=entry)
60
+ if spec.name=='batch.atomic':
61
+ # Validate the child set before an old cached parent result
62
+ # can bypass the handler's secret-delivery exclusion.
63
+ packets(self.registry,request,principal.subject,
64
+ self.max_request_bytes)
65
+ context=ExecutionContext(request_id=request.request_id,principal=principal,entry=entry,
66
+ now=self.clock(),deadline_monotonic=time.monotonic()+30)
67
+ checks=await spec.requirements(request,session)
68
+ await self.authorizer.require(context,request,checks,session)
69
+ previous=None
70
+ if spec.effect!='read':
71
+ previous=await session.request_result(principal.subject,request.request_id,request.payload_digest)
72
+ if previous is not None:
73
+ # Idempotency is not a cached authorization decision. Results
74
+ # can contain references to content whose access changed.
75
+ visible=tuple(AccessRequirement(resource_id=ref.id,operation=f'{spec.name}@{spec.version}',check='read')
76
+ for ref in previous.resources)
77
+ await self.authorizer.require(context,request,visible,session)
78
+ result=replace(previous,replayed=True)
79
+ else:
80
+ capacity_writes={'identity.register','identity.temporary','identity.custodial_create','content.topic_create',
81
+ 'content.post_create','content.post_edit','content.file_put','content.attach',
82
+ 'content.template_put','discussion.reply','discussion.quote','discussion.repost',
83
+ 'transfer.part_put','git.create','git.push','git.receive','hosting.deploy','hosting.preview','keystore.put','achievement.start'}
84
+ if spec.name in capacity_writes:
85
+ require(session.setting('runtime_config',{}).get('accept_writes',True),'writes_paused')
86
+ for rid,generation in request.expected_generations:
87
+ current=await session.resource(rid)
88
+ if rid not in {check.resource_id for check in checks}:
89
+ check='manage' if current.owner==principal.subject else 'read'
90
+ await self.authorizer.require(context,request,(AccessRequirement(resource_id=rid,
91
+ operation=f'{spec.name}@{spec.version}',check=check),),session)
92
+ require(current.generation==generation,'generation_conflict',
93
+ details={'id':rid,'generation':current.generation,'revision':current.revision})
94
+ audited=(spec.name in {'content.chmod','content.chgrp','content.chown','content.purge','content.move',
95
+ 'identity.key_add','identity.key_revoke','identity.recover','identity.delegate','identity.delegation_revoke',
96
+ 'identity.ssh_key_add','identity.ssh_key_revoke','identity.ssh_certificates','cert.publish','cert.request'}
97
+ or (spec.name.startswith('group.') and spec.effect!='read'))
98
+ before=[]
99
+ if audited:
100
+ ids={c.resource_id for c in checks}|{principal.subject}
101
+ for resource_id in sorted(ids):
102
+ try:before.append(wire(await session.resource(resource_id)))
103
+ except Failure as exc:
104
+ if exc.code not in {'not_found'}:raise
105
+ output=await spec.handler(context,request,session)
106
+ if request.return_fields:
107
+ require(output.resources and self.result_projection is not None,'projection_unavailable')
108
+ projections=[await self.result_projection(context,request,session,ref,
109
+ fields=request.return_fields) for ref in output.resources]
110
+ output=replace(output,data={**(output.data or {}),'projection':projections})
111
+ if audited:
112
+ from msg.core.models import AuditEvent,ResourceRef
113
+ audit_event=Event(id='a_'+uuid4().hex,type=spec.name,time=self.clock(),request_id=request.request_id,
114
+ actor=principal.actor,subject=principal.subject,resources=output.resources,
115
+ data={'request_digest':request.payload_digest})
116
+ await session.append_audit(AuditEvent(event=audit_event,
117
+ authority=tuple(ResourceRef(id=c) for c in principal.certificates),before_digest=digest(before),
118
+ after_digest=digest(output),previous_digest=None,entry_digest='',result='committed'))
119
+ require(isinstance(output,HandlerOutput),'invalid_handler_output')
120
+ self.registry.validate(spec.output_schema,wire(output))
121
+ result=OperationResult(request_id=request.request_id,operation=spec.name,
122
+ status='accepted' if spec.effect=='external' else 'ok',actor=principal.actor,subject=principal.subject,
123
+ resources=output.resources,committed_at=self.clock() if spec.effect!='read' else None,
124
+ data=output.data,output=output.output)
125
+ if spec.effect!='read':
126
+ event=Event(id=event_id(request,principal.subject),type=spec.name,time=result.committed_at,request_id=request.request_id,
127
+ actor=principal.actor,subject=principal.subject,resources=output.resources,
128
+ data={'operation':spec.name})
129
+ await session.append_event(event)
130
+ if self.event_notifications is not None:
131
+ await self.event_notifications(session,event)
132
+ result=replace(result,receipt=self.receipt_signer.sign(receipt_bytes(result),purpose='receipt'))
133
+ await session.save_result(principal.subject,request.payload_digest,result)
134
+ # Only after the enclosing transaction commits may success reach the adapter.
135
+ result=replace(result,prefer_cli=request.source!='msg',
136
+ cli_url='/AGENTS.md' if request.source!='msg' else None)
137
+ if self.response_hook is not None:
138
+ result=await self.response_hook(request,result)
139
+ return result
140
+ except Failure as exc:
141
+ return OperationResult(request_id=request.request_id,operation=request.operation,status='error',
142
+ actor=principal.actor if principal else None,subject=principal.subject if principal else None,
143
+ error=OperationError(code=exc.code,retryable=exc.retryable,field_path=exc.field),
144
+ data=exc.details)
145
+ except Exception:
146
+ # Neither exception text/tracebacks nor caller-selected IDs are safe
147
+ # log fields. The stable event remains countable without secrets.
148
+ log.error('operation_failed')
149
+ return OperationResult(request_id=request.request_id,operation=request.operation,status='error',
150
+ actor=principal.actor if principal else None,subject=principal.subject if principal else None,
151
+ error=OperationError(code='internal_error',retryable=False))
152
+
153
+
154
+ async def _independent(self,request,spec,entry):
155
+ """Reserve an idempotency key, commit children separately, then the result.
156
+
157
+ Interrupted batches are resumed by resending the original signed children.
158
+ No child is reported rolled back merely because its sibling failed.
159
+ """
160
+ async with self.metadata.transaction(write=True) as tx:
161
+ principal=await self.authenticator.authenticate(request,tx,entry=entry)
162
+ await self.authorizer._ceiling(principal,f'{spec.name}@{spec.version}',principal.subject,tx)
163
+ children=packets(self.registry,request,principal.subject,self.max_request_bytes)
164
+ existing=tx.one('SELECT digest FROM batches WHERE subject=? AND request_id=?',(principal.subject,request.request_id))
165
+ require(existing is None or existing[0]==request.payload_digest,'idempotency_conflict')
166
+ previous=await tx.request_result(principal.subject,request.request_id,request.payload_digest)
167
+ if previous is not None:
168
+ context=ExecutionContext(request_id=request.request_id,principal=principal,entry=entry,now=self.clock(),deadline_monotonic=time.monotonic()+30)
169
+ await self.authorizer.require(context,request,tuple(AccessRequirement(resource_id=ref.id,
170
+ operation=f'{spec.name}@{spec.version}',check='read') for ref in previous.resources),tx)
171
+ return replace(previous,replayed=True,prefer_cli=request.source!='msg',cli_url='/AGENTS.md' if request.source!='msg' else None)
172
+ tx.execute('INSERT OR IGNORE INTO batches VALUES (?,?,?)',(principal.subject,request.request_id,request.payload_digest),write=True)
173
+ results=[];resources=[]
174
+ for child in children:
175
+ result=await self.execute(child,entry=entry)
176
+ results.append(result_wire(result))
177
+ resources.extend(result.resources)
178
+ async with self.metadata.transaction(write=True) as tx:
179
+ current=await self.authenticator.authenticate(request,tx,entry=entry)
180
+ await self.authorizer._ceiling(current,f'{spec.name}@{spec.version}',current.subject,tx)
181
+ previous=await tx.request_result(principal.subject,request.request_id,request.payload_digest)
182
+ if previous is not None:
183
+ context=ExecutionContext(request_id=request.request_id,principal=current,entry=entry,now=self.clock(),deadline_monotonic=time.monotonic()+30)
184
+ await self.authorizer.require(context,request,tuple(AccessRequirement(resource_id=ref.id,
185
+ operation=f'{spec.name}@{spec.version}',check='read') for ref in previous.resources),tx)
186
+ return replace(previous,replayed=True,prefer_cli=request.source!='msg',cli_url='/AGENTS.md' if request.source!='msg' else None)
187
+ result=OperationResult(request_id=request.request_id,operation=spec.name,status='ok',actor=principal.actor,
188
+ subject=principal.subject,resources=tuple(dict.fromkeys(resources)),data={'results':results,'atomic':False},committed_at=self.clock())
189
+ result=replace(result,receipt=self.receipt_signer.sign(receipt_bytes(result),purpose='receipt'))
190
+ await tx.append_event(Event(id=event_id(request,principal.subject),type=spec.name,time=result.committed_at,
191
+ request_id=request.request_id,actor=principal.actor,subject=principal.subject,resources=result.resources,data={'operation':spec.name}))
192
+ await tx.save_result(principal.subject,request.payload_digest,result)
193
+ return replace(result,prefer_cli=request.source!='msg',cli_url='/AGENTS.md' if request.source!='msg' else None)