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/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
|