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
@@ -0,0 +1,150 @@
1
+ """Certificate-scoped tool discovery and durable explicit invocation."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import replace
5
+
6
+ from msg.constants import TOOLS_SPACE
7
+ from msg.core.codec import decode, digest, loads, wire
8
+ from msg.core.errors import Failure, require
9
+ from msg.core.models import EffectJob, HandlerOutput, NetworkPolicy, ResourceRef, ToolSpec
10
+ from msg.plugins.common import check_access, new_id, operation_id, resolve
11
+ from msg.plugins.communication import event_id
12
+ from msg.plugins.schemas import BYTES, IDENTIFIER, REF, obj
13
+ from msg.security.network import intersect_policy, normalized_host, validate_addresses, validate_url
14
+ from msg.security.policy import grant_covers
15
+
16
+ DNS_INPUT=obj({'name':{'type':'string','minLength':1,'maxLength':253},'type':{'enum':['A','AAAA','TXT','MX','NS','CNAME','SRV']}},('name','type'))
17
+ CURL_INPUT=obj({'url':{'type':'string','minLength':1,'maxLength':8192},
18
+ 'method':{'enum':['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS']},
19
+ 'headers':{'type':'object','maxProperties':30,'additionalProperties':{'type':'string','maxLength':4096}},
20
+ 'body':BYTES,'body_ref':REF},('url',))
21
+ TOOL_OUTPUT={'type':'object'}
22
+
23
+
24
+ def descriptor(name):
25
+ return {'tool_id':'tool_'+name,'version':1,'executor_key':name,'operation':'tool.run',
26
+ 'required_capabilities':['tool.use'],'input_schema':{'id':'schema:tool.'+name+':input'},
27
+ 'output_schema':{'id':'schema:tool.'+name+':output'},
28
+ 'network':{'schemes':['http','https'],'hosts':[],'ports':[80,443],
29
+ 'methods':['GET','HEAD','POST','PUT','PATCH','DELETE','OPTIONS'],
30
+ 'allow_private':True,'timeout_ms':30000,'max_response_bytes':16777216,'max_redirects':5},
31
+ 'default_targets':'public','description':'Structured DNS query' if name=='dns' else 'Bounded HTTP(S) request; not a shell'}
32
+
33
+
34
+ async def resolve_tool_for_invoke(tx,value):
35
+ if isinstance(value,str) and (value=='/_tools' or value.startswith('/_tools/')):
36
+ raise Failure('legacy_tool_path_read_only')
37
+ if isinstance(value,str) and (value=='/tools' or value.startswith('/tools/')):
38
+ try:
39
+ return await resolve(tx,value)
40
+ except Failure as exc:
41
+ if exc.code!='not_found':
42
+ raise
43
+ # Old databases retain their original path. The canonical invocation
44
+ # spelling still resolves to the same stable IDs and capability checks.
45
+ directory=await tx.resource(TOOLS_SPACE)
46
+ if directory.name!='_tools':
47
+ raise
48
+ return await tx.resolve('/_tools'+value[len('/tools'):])
49
+ return await resolve(tx,value)
50
+
51
+
52
+ async def read_tool(app,tx,rid,revision=None):
53
+ resource=await tx.resource(rid)
54
+ require(resource.type=='tool' and resource.parent==TOOLS_SPACE,'not_a_tool')
55
+ rev=await tx.revision(ResourceRef(id=rid,revision=revision))
56
+ value=loads(await app.contents.read_bytes(rev.content,limit=65536))
57
+ require(value['executor_key'] in {'dns','curl'} and value['tool_id']==rid,'untrusted_tool_executor')
58
+ return ToolSpec(resource=ResourceRef(id=rid,revision=rev.id),operation='tool.run',
59
+ input_schema=decode(ResourceRef,value['input_schema']),output_schema=decode(ResourceRef,value['output_schema']),
60
+ executor_key=value['executor_key'],network=decode(NetworkPolicy,value['network']))
61
+
62
+
63
+ async def tool_policies(app,principal,tool,args,tx):
64
+ operation='tool.run@1'
65
+ grants=await app.authorizer.grants(principal,tx)
66
+ certs=[g for g in grants if await grant_covers(g,'tool.use',operation,tool.resource.id,tx)]
67
+ ceilings=[g for g in principal.ceiling if await grant_covers(g,'tool.use',operation,tool.resource.id,tx)]
68
+ private=[g for g in grants if await grant_covers(g,'tool.net.private',operation,tool.resource.id,tx)]
69
+ private_ceil=[g for g in principal.ceiling if await grant_covers(g,'tool.net.private',operation,tool.resource.id,tx)]
70
+ require(certs and ceilings,'tool_certificate_required')
71
+ deployment={'timeout_ms':app.settings.tool_timeout_ms,'max_response_bytes':app.settings.tool_max_response_bytes,
72
+ 'methods':app.settings.tool_methods,'ports':app.settings.tool_ports}
73
+ base=intersect_policy(tool.network,deployment)
74
+ candidates=[]
75
+ for grant in certs:
76
+ for ceiling in ceilings:
77
+ try:
78
+ value=intersect_policy(intersect_policy(base,grant.constraints),ceiling.constraints)
79
+ candidates.append(replace(value,allow_private=False))
80
+ if base.allow_private:
81
+ for private_grant in private:
82
+ for private_ceiling in private_ceil:
83
+ candidates.append(replace(intersect_policy(intersect_policy(value,private_grant.constraints),
84
+ private_ceiling.constraints),allow_private=True))
85
+ except Failure as exc:
86
+ if exc.code!='network_policy_empty':raise
87
+ allowed=[]
88
+ for policy in candidates:
89
+ try:
90
+ if tool.executor_key=='curl':
91
+ parsed=validate_url(args['url'],args.get('method','GET'),policy)
92
+ import ipaddress
93
+ try:address=ipaddress.ip_address(parsed.hostname)
94
+ except ValueError:address=None
95
+ if address is not None:validate_addresses([str(address)],policy)
96
+ else:
97
+ host=normalized_host(args['name'])
98
+ require(not policy.hosts or host in policy.hosts,'network_host_forbidden')
99
+ allowed.append(policy)
100
+ except Failure:
101
+ continue
102
+ require(allowed,'network_policy_denied')
103
+ # Alternatives preserve unions of grants; the child checks every resolution
104
+ # and redirect against the complete conjunction selected for that target.
105
+ return tuple(allowed)
106
+
107
+
108
+ def register(app,op):
109
+ for name,schema in (('dns',DNS_INPUT),('curl',CURL_INPUT)):
110
+ app.registry.add_schema(ResourceRef(id='schema:tool.'+name+':input'),schema)
111
+ app.registry.add_schema(ResourceRef(id='schema:tool.'+name+':output'),TOOL_OUTPUT)
112
+
113
+ @op('tool.run',obj({'id':IDENTIFIER,'revision':IDENTIFIER,'arguments':{'type':'object'}},('id','arguments')),effect='external')
114
+ async def invoke(ctx,request,tx):
115
+ rid=await resolve_tool_for_invoke(tx,request.arguments['id'])
116
+ await check_access(app,ctx,request,tx,rid,'tool_use')
117
+ tool=await read_tool(app,tx,rid,request.arguments.get('revision'))
118
+ args=request.arguments['arguments']
119
+ app.registry.validate(tool.input_schema,args)
120
+ policies=await tool_policies(app,ctx.principal,tool,args,tx)
121
+ if args.get('body_ref') is not None:
122
+ ref=decode(ResourceRef,args['body_ref'])
123
+ require(ref.revision is not None,'source_revision_required')
124
+ await check_access(app,ctx,request,tx,ref.id,'read')
125
+ await tx.revision(ref)
126
+ require(not ('body' in args and 'body_ref' in args),'ambiguous_request_body')
127
+ eid=event_id(request,ctx.principal.subject)
128
+ job=EffectJob(id=new_id('job'),event_id=eid,kind='tool',dedupe_key='tool:'+eid,principal=ctx.principal,
129
+ operation=request.operation,arguments={'tool':wire(tool.resource),'input':wire(args),
130
+ 'policies':wire(policies),'request_id':request.request_id},state='pending',attempts=0,
131
+ next_attempt_at=ctx.now,lease_until=None)
132
+ await tx.enqueue(job)
133
+ return HandlerOutput(resources=(tool.resource,),data={'job_id':job.id,'state':'pending','tool_id':rid,
134
+ 'input_digest':digest(args),'target_digest':digest(args.get('url',args.get('name','')))})
135
+
136
+ @op('job.get',obj({'id':IDENTIFIER},('id',)),effect='read')
137
+ async def job_get(ctx,request,tx):
138
+ job=await tx.job(request.arguments['id'])
139
+ require(job.principal.subject==ctx.principal.subject,'job_owner_required')
140
+ if job.kind=='tool':
141
+ await check_access(app,ctx,request,tx,job.arguments['tool']['id'],'read')
142
+ else:
143
+ await app.authorizer._ceiling(ctx.principal,operation_id(request),ctx.principal.subject,tx)
144
+ data={'id':job.id,'state':job.state,'attempts':job.attempts,'event_id':job.event_id}
145
+ if job.result:
146
+ await check_access(app,ctx,request,tx,job.result.id,'read')
147
+ data['output']=wire(job.result)
148
+ status=tx.setting('job_status:'+job.id)
149
+ if status:data['result']=status
150
+ return HandlerOutput(data=data,output=job.result)
msg/hosting_runtime.py ADDED
@@ -0,0 +1,138 @@
1
+ """Explicit read-only composition for the independent hosting HTTP process."""
2
+ from dataclasses import replace
3
+ from datetime import UTC, datetime
4
+ from functools import partial
5
+ from types import SimpleNamespace
6
+ import os
7
+
8
+ import psycopg
9
+
10
+ from msg.constants import ROOT_SUBJECT
11
+ from msg.core.codec import decode, digest, loads, unb64
12
+ from msg.core.errors import Failure, require
13
+ from msg.core.models import Certificate, ResourceRef
14
+ from msg.core.registry import Registry
15
+ from msg.plugins import install_registry
16
+ from msg.security.authentication import AuthenticationService
17
+ from msg.security.authorization import AuthorizationService
18
+ from msg.security.capabilities import primary_ceiling, temporary_ceiling
19
+ from msg.security.certificates import CertificateValidator
20
+ from msg.security.quarantine import require_live_authority
21
+ from msg.storage.read_only import GitContentReader, ReadOnlyPostgresStore
22
+
23
+
24
+ async def _no_execution(*args, **kwargs):
25
+ raise Failure('readonly_registry')
26
+
27
+
28
+ def _contract_only(spec):
29
+ if spec.handler is _no_execution and spec.requirements is _no_execution:
30
+ return spec
31
+ return replace(spec, handler=_no_execution, requirements=_no_execution)
32
+
33
+
34
+ class _ContractRegistry(Registry):
35
+ """Use the original contracts/validation but retain no business closures."""
36
+ def add_operation(self, spec):
37
+ super().add_operation(_contract_only(spec))
38
+
39
+ def add(self, manifest):
40
+ super().add(replace(manifest, operations=tuple(
41
+ _contract_only(spec) for spec in manifest.operations)))
42
+
43
+
44
+ async def require_prepared_sources(tx):
45
+ """The installer owns publication; this role only compares its release inputs."""
46
+ from msg.bootstrap import RULE_SPECS, SOURCE_PATH_OVERRIDES, SOURCE_HEADER, system_source_root
47
+ root = system_source_root()
48
+ expected = set()
49
+ for rule_id, rid, _, _, default_path, _ in RULE_SPECS:
50
+ path = SOURCE_PATH_OVERRIDES.get(rule_id, default_path)
51
+ raw = root.joinpath(path).read_bytes()
52
+ header = SOURCE_HEADER.match(raw.decode('utf-8'))
53
+ require(header is not None and header.group(1) == rule_id,
54
+ 'hosting_installation_stale')
55
+ row = tx.one('''SELECT source_path,rule_id,source_kind,source_version,source_digest,revision_id
56
+ FROM system_sources WHERE resource_id=?''', (rid,))
57
+ require(row is not None and row[:5] == (
58
+ 'docs/system/' + path, rule_id, 'release', int(header.group(2)), digest(raw)),
59
+ 'hosting_installation_stale')
60
+ resource = await tx.resource(rid)
61
+ require(resource.revision == row[5], 'hosting_installation_stale')
62
+ revision = await tx.revision(ResourceRef(id=rid, revision=row[5]))
63
+ require(revision.source_kind == 'release' and revision.source_digest == row[4]
64
+ and revision.source_version == row[3], 'hosting_installation_stale')
65
+ expected.add(rid)
66
+ require({row[0] for row in tx.rows('SELECT resource_id FROM system_sources')} == expected,
67
+ 'hosting_installation_stale')
68
+
69
+
70
+ class HostingRuntime:
71
+ """Current verification + existing bytes. No Application, issuer or executor."""
72
+ readonly_hosting = True
73
+
74
+ def __init__(self, settings, *, clock=None):
75
+ self.settings = settings
76
+ self.clock = clock or (lambda: datetime.now(UTC))
77
+ self.registry = _ContractRegistry()
78
+ # Installers only register trusted code. The throw-away context has no
79
+ # storage, signer, policy service or parent Application to capture.
80
+ install_registry(SimpleNamespace(registry=self.registry), settings.server.plugins)
81
+ self.metadata = None
82
+ self.contents = None
83
+ self._loaded = False
84
+ self._quarantined = False
85
+ self._marker = settings.hosting_recovery_marker or settings.config_dir / 'recovery-drill.json'
86
+
87
+ def require_ready(self, *, loading=False):
88
+ require(loading or self._loaded, 'hosting_not_ready')
89
+ try:
90
+ self._marker.lstat() # includes a dangling symlink: never a bypass
91
+ except FileNotFoundError:
92
+ require(self._marker.parent.is_dir() and os.access(self._marker.parent, os.X_OK),
93
+ 'hosting_recovery_state_unavailable')
94
+ except OSError as exc:
95
+ raise Failure('hosting_recovery_state_unavailable') from exc
96
+ else:
97
+ self._quarantined = True
98
+ require(not self._quarantined, 'recovery_quarantined')
99
+
100
+ async def load(self):
101
+ require(not self._loaded, 'hosting_already_loaded')
102
+ require(self.settings.public_web_origin is not None, 'hosting_origin_not_configured')
103
+ self.require_ready(loading=True)
104
+ require(self.settings.trust_file.is_file(), 'root_not_initialized')
105
+ trust = loads(self.settings.trust_file.read_bytes())
106
+ require(type(trust) is dict and set(trust) == {'version', 'public_key', 'certificate'}
107
+ and trust['version'] == 1, 'invalid_trust_anchor')
108
+ root = decode(Certificate, trust['certificate'])
109
+ public = unb64(trust['public_key'], limit=32)
110
+ require(len(public) == 32, 'invalid_trust_anchor')
111
+ self.contents = GitContentReader(self.settings.server.content_dir,
112
+ binary_dir=self.settings.server.blob_dir)
113
+ self.metadata = ReadOnlyPostgresStore(self.settings.server.postgres_dsn)
114
+ self.certificates = CertificateValidator(self.registry, root, public,
115
+ self.settings.service_url, self.clock)
116
+ try:
117
+ async with self.metadata.transaction(write=False) as tx:
118
+ require_live_authority(tx)
119
+ require(tx.setting('active_root_certificate', root.resource_id) == root.resource_id,
120
+ 'service_restart_required')
121
+ await self.certificates.validate(root.resource_id, tx)
122
+ require((await tx.subject(ROOT_SUBJECT)).local_only, 'root_policy_corrupt')
123
+ await require_prepared_sources(tx)
124
+ except (psycopg.errors.UndefinedTable, psycopg.errors.UndefinedColumn,
125
+ psycopg.errors.InsufficientPrivilege) as exc:
126
+ raise Failure('hosting_installation_stale') from exc
127
+ self.authenticator = AuthenticationService(self.registry, self.certificates,
128
+ self.settings.service_url, self.clock,
129
+ partial(primary_ceiling, self.registry), partial(temporary_ceiling, self.registry))
130
+ self.authorizer = AuthorizationService(self.registry, self.certificates)
131
+ self.require_ready(loading=True)
132
+ self._loaded = True
133
+ return self
134
+
135
+ async def close(self):
136
+ self._loaded = False
137
+ if self.metadata is not None:
138
+ await self.metadata.close()
msg/market/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Versioned market facts; only the executor/worker owns SQL transactions."""