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/plugins/common.py ADDED
@@ -0,0 +1,217 @@
1
+ """Explicit resource use cases shared by content and extensions."""
2
+ from __future__ import annotations
3
+ import re
4
+ from dataclasses import replace
5
+ from uuid import uuid4
6
+ from msg.constants import *
7
+ from msg.core.codec import canonical,wire,digest,decode,loads,parse_time
8
+ from msg.core.errors import Failure,require
9
+ from msg.core.models import Resource,ResourceRef,Revision,AccessRequirement,HandlerOutput,Signature
10
+ from msg.security.crypto import verify
11
+ from msg.security.policy import inherit_group,SETGID
12
+
13
+
14
+ def new_id(prefix='r'):
15
+ return prefix+'_'+uuid4().hex
16
+
17
+
18
+ def operation_id(request):
19
+ return f'{request.operation}@{request.contract_version}'
20
+
21
+
22
+ async def resolve(session,value):
23
+ if isinstance(value,str) and value.startswith('/'):
24
+ return await session.resolve(value)
25
+ require(isinstance(value,str) and bool(re.fullmatch(r'[A-Za-z0-9_.:-]{1,160}',value)),'invalid_resource_id')
26
+ return (await session.resource(value)).id
27
+
28
+
29
+ async def check_access(app,context,request,session,rid,check):
30
+ return await app.authorizer.require(context,request,(AccessRequirement(resource_id=rid,
31
+ operation=operation_id(request),check=check),),session)
32
+
33
+
34
+ def requirement(argument,check, *, default=None):
35
+ async def requirements(request,session):
36
+ target=request.arguments.get(argument,default)
37
+ rid=await resolve(session,target)
38
+ return (AccessRequirement(resource_id=rid,operation=operation_id(request),check=check),)
39
+ return requirements
40
+
41
+
42
+ async def no_requirements(request,session):
43
+ return ()
44
+
45
+
46
+ def validate_name(name, *, identity=False):
47
+ require(isinstance(name,str) and 1<=len(name)<=120 and '/' not in name and '\\' not in name and
48
+ name not in {'.','..','json','meta','raw','history','revisions'} and
49
+ all(ord(c)>=32 and ord(c)!=127 for c in name),'invalid_name')
50
+ if not identity:
51
+ require(not name.startswith(('@','&','!','~')),'reserved_name')
52
+ return name
53
+
54
+
55
+ async def protect_namespace(app,ctx,request,tx,parent,name):
56
+ if name.startswith('_'):
57
+ require(await app.authorizer.has(ctx.principal,'system.namespace',operation_id(request),parent.id,tx),
58
+ 'protected_namespace')
59
+
60
+
61
+ async def create_resource(app,ctx,request,tx, *, parent,type,name=None,body=None,media_type='text/markdown',
62
+ relations=(),mode=None,resource_id=None,author=None,content_signature=None,revision_id=None):
63
+ parent=await tx.resource(parent)
64
+ require(parent.state=='active','ancestor_inactive')
65
+ require(app.registry.resource_type(parent.type,parent.type_version).container,'not_a_container')
66
+ app.registry.resource_type(type,1)
67
+ if parent.id=='t_store' or type=='listing':
68
+ require(parent.id=='t_store' and type=='listing' and
69
+ request.operation in {'store.listing_create','bounty.create'},
70
+ 'store_controlled_resource')
71
+ if parent.id=='t_last_will':
72
+ require(request.operation=='identity.legacy_put' and type=='legacy_directive',
73
+ 'legacy_directive_only')
74
+ if type=='post':
75
+ name=(name or new_id('p'))
76
+ if not name.endswith('.md'):
77
+ name += '.md'
78
+ name=validate_name(name or new_id('p'))
79
+ if parent.type=='user' and name in {'SOUL.md','AGENTS.md','notes','todos'}:
80
+ require(request.operation in {'identity.personal_put','identity.note_put','identity.todo_put'},
81
+ 'personal_managed_resource')
82
+ if (parent.name=='notes' and parent.parent is not None and
83
+ (await tx.resource(parent.parent)).type=='user'):
84
+ require(request.operation=='identity.note_put','personal_managed_resource')
85
+ if (parent.name=='todos' and parent.parent is not None and
86
+ (await tx.resource(parent.parent)).type=='user'):
87
+ require(request.operation=='identity.todo_put' and type=='todo',
88
+ 'personal_managed_resource')
89
+ await protect_namespace(app,ctx,request,tx,parent,name)
90
+ principal=ctx.principal
91
+ subject=await tx.subject(principal.subject)
92
+ policy=tx.setting('policy:'+parent.id,{})
93
+ container=app.registry.resource_type(type,1).container
94
+ default=policy.get(type+'_mode',policy.get('topic_mode' if container else 'post_mode','1777' if container else '0644'))
95
+ mode=int(default,8) if mode is None else mode
96
+ if container and parent.mode&SETGID:
97
+ mode|=SETGID
98
+ rid=resource_id or new_id()
99
+ require(bool(re.fullmatch(r'[A-Za-z0-9_-]{1,128}',rid)),'invalid_resource_id')
100
+ resource=Resource(id=rid,type=type,type_version=1,name=name,parent=parent.id,owner=subject.resource_id,
101
+ group=inherit_group(parent,subject.primary_group),mode=mode,generation=0,revision=None,state='active',
102
+ created_at=ctx.now,created_by=principal.actor,modified_at=ctx.now,modified_by=principal.actor)
103
+ await tx.insert(resource)
104
+ if body is not None:
105
+ resource=await revise_resource(app,ctx,request,tx,resource,body,media_type,relations=relations,
106
+ author=author,signature=content_signature,revision_id=revision_id)
107
+ return resource
108
+
109
+
110
+ async def revise_resource(app,ctx,request,tx,resource,body,media_type='text/markdown', *, relations=(),author=None,
111
+ signature=None,revision_id=None,change_note=None,source_kind=None,
112
+ source_version=None,source_digest=None,content_created_at=None):
113
+ from msg.core.models import BlobRef
114
+ # Internal operation labels (e.g. transfer.seal publishing) carry no client arguments.
115
+ timestamp=(content_created_at if content_created_at is not None else
116
+ getattr(request,'arguments',{}).get('content_created_at'))
117
+ # A client-chosen Revision ID or content time is only defined as part of the
118
+ # signed manifest; without the signature it would be an unproven claim.
119
+ require(signature is not None or (revision_id is None and timestamp is None),
120
+ 'content_signature_required')
121
+ require(signature is None or revision_id is not None,'revision_id_required')
122
+ if isinstance(body,BlobRef):
123
+ blob=body
124
+ else:
125
+ blob=await app.contents.put_bytes(body.encode('utf-8') if isinstance(body,str) else body,media_type)
126
+ for relation in relations:
127
+ spec=app.registry.resource_type(resource.type,resource.type_version)
128
+ require(relation.type in spec.relations,'invalid_relation_type')
129
+ await check_access(app,ctx,request,tx,relation.target.id,'read')
130
+ if relation.target.revision is not None:
131
+ await tx.revision(relation.target)
132
+ rid=revision_id or new_id('v')
133
+ require(bool(re.fullmatch(r'[A-Za-z0-9_-]{1,128}',rid)),'invalid_revision_id')
134
+ content_time=ctx.now
135
+ if signature is not None:
136
+ require(timestamp is not None,'content_timestamp_required')
137
+ content_time=parse_time(timestamp)
138
+ require(abs((content_time-ctx.now).total_seconds())<=300,'content_timestamp_out_of_range')
139
+ revision=Revision(format_version=1,id=rid,resource_id=resource.id,parents=(resource.revision,) if resource.revision else (),
140
+ content=blob,relations=tuple(relations),actor=ctx.principal.actor,subject=ctx.principal.subject,
141
+ author=author or ctx.principal.subject,created_at=content_time,manifest_digest='',
142
+ change_note=change_note,source_kind=source_kind,source_version=source_version,source_digest=source_digest)
143
+ custodial=(ctx.principal.method=='token' and
144
+ (await tx.subject(ctx.principal.subject)).kind=='custodial')
145
+ if custodial:
146
+ require(signature is None,'custodial_content_signature_forbidden')
147
+ revision=replace(revision,signature_source='custodial',source_kind='operation',
148
+ source_version=request.contract_version,source_digest=request.payload_digest)
149
+ body_to_sign={k:v for k,v in wire(revision).items() if k not in {'manifest_digest','signature'}}
150
+ revision=replace(revision,manifest_digest=digest(body_to_sign))
151
+ if signature is not None:
152
+ sig=decode(Signature,signature)
153
+ credential=await tx.credential(ctx.principal.credential_id)
154
+ require(sig.key_id==credential.id,'content_signer_mismatch')
155
+ verify(credential.verifier,canonical(body_to_sign),sig,purpose='revision')
156
+ revision=replace(revision,signature=sig)
157
+ elif custodial:
158
+ from msg.security.vault import open_signer
159
+ signer=open_signer(app,tx,ctx.principal.subject)
160
+ primary=tx.one('SELECT key_id FROM identity_keys WHERE subject=? AND is_primary=1',
161
+ (ctx.principal.subject,))
162
+ require(primary is not None and primary[0]==signer.key_id,'custodial_vault_key_mismatch')
163
+ revision=replace(revision,signature=signer.sign(canonical(body_to_sign),purpose='revision'))
164
+ if revision_id is None or not await app.contents.pinned(blob,rid):
165
+ tx.on_rollback(lambda: app.contents.unpin(blob,rid))
166
+ await app.contents.pin(blob,rid)
167
+ ancestors=await tx.ancestors(resource.id)
168
+ topic=next((p.id for p in reversed(ancestors) if p.type=='topic'),ROOT_SPACE)
169
+ await app.contents.commit_revision(topic,revision)
170
+ await tx.append_revision(revision)
171
+ updated=replace(resource,generation=resource.generation+1,revision=rid,modified_at=ctx.now,modified_by=ctx.principal.actor)
172
+ await tx.replace(updated,resource.generation)
173
+ if media_type.startswith('text/') and blob.size<=1048576:
174
+ text=(await app.contents.read_bytes(blob)).decode('utf-8',errors='replace')
175
+ tx.execute('INSERT INTO projections VALUES (?,?) ON CONFLICT(resource_id) DO UPDATE SET text=excluded.text',
176
+ (resource.id,text),write=True)
177
+ return updated
178
+
179
+
180
+ def output_for(resource,**data):
181
+ return HandlerOutput(resources=(ResourceRef(id=resource.id,revision=resource.revision),),
182
+ data={'generation':resource.generation,**data})
183
+
184
+
185
+ async def assert_generation(request,resource):
186
+ values=dict(request.expected_generations)
187
+ require(resource.id in values,'expected_generation_required')
188
+ require(values[resource.id]==resource.generation,'generation_conflict',
189
+ details={'id':resource.id,'generation':resource.generation,'revision':resource.revision})
190
+
191
+
192
+ async def topic_policy(tx,resource):
193
+ topic=resource if resource.type=='topic' else await tx.resource(resource.parent)
194
+ return tx.setting('policy:'+topic.id,{})
195
+
196
+
197
+ def registration(app,name,dependencies=()):
198
+ from msg.core.models import OperationSpec,PluginManifest,ResourceRef
199
+ from msg.plugins.schemas import OUTPUT
200
+ operations=[]
201
+ output_ref=ResourceRef(id='schema:operation-result')
202
+ if output_ref.id not in app.registry._schemas:
203
+ app.registry.add_schema(output_ref,OUTPUT)
204
+ def operation(opname,schema, *, effect='transaction',requirements=no_requirements,signature=False,version=1):
205
+ def decorate(handler):
206
+ ref=ResourceRef(id='schema:'+opname+':'+str(version))
207
+ app.registry.add_schema(ref,schema)
208
+ operations.append(OperationSpec(name=opname,version=version,input_schema=ref,output_schema=output_ref,
209
+ effect=effect,entries=frozenset({'network','worker'}),require_signature=signature,
210
+ requirements=requirements,handler=handler))
211
+ return handler
212
+ return decorate
213
+ def finish(resource_types=()):
214
+ manifest=PluginManifest(name=name,version='1',dependencies=tuple(dependencies),
215
+ resource_types=tuple(resource_types),capabilities=(),operations=tuple(operations),migrations=())
216
+ app.registry.add(manifest)
217
+ return operation,finish