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