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/extensions/tools.py
ADDED
|
@@ -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."""
|