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,58 @@
1
+ """Isolated selftest of the real upgrade commit and new-key recovery boundary."""
2
+ from datetime import timedelta
3
+ import hashlib
4
+ import os
5
+ from uuid import uuid4
6
+
7
+ from msg.core.codec import b64, canonical, unb64, wire
8
+ from msg.core.requests import request_for
9
+ from msg.security.age_keys import generate_age_key
10
+ from msg.security.crypto import Ed25519Signer
11
+
12
+
13
+ async def check_upgrade_recovery(app, now):
14
+ signer = Ed25519Signer.generate()
15
+ _, recipient = generate_age_key()
16
+ nonce, recovery = b64(os.urandom(32)), b64(os.urandom(32))
17
+ subject = 'u_tmp_' + hashlib.sha256(unb64(nonce)).hexdigest()[:32]
18
+ request_id = uuid4().hex
19
+ proof = signer.sign(canonical({'subject_id': subject, 'nonce': nonce,
20
+ 'public_key': b64(signer.public_key), 'encryption_recipient': recipient,
21
+ 'request_id': request_id}), purpose='temporary-key-possession-v1')
22
+ created = await app.executor.execute(request_for('identity.temporary', {
23
+ 'nonce': nonce, 'recovery_secret': recovery, 'public_key': b64(signer.public_key),
24
+ 'encryption_recipient': recipient, 'possession_proof': wire(proof)},
25
+ app.settings.service_url, request_id=request_id, contract_version=3,
26
+ expires_at=now + timedelta(seconds=120)))
27
+ if created.status != 'ok':
28
+ return False
29
+ token = created.data['credential_id'], unb64(created.data['token'])
30
+ upgrade_id = uuid4().hex
31
+ signed = {'subject_id': subject, 'handle': 'selftest-upgrade',
32
+ 'public_key': b64(signer.public_key), 'encryption_recipient': recipient}
33
+ arguments = {key: value for key, value in signed.items() if key != 'subject_id'}
34
+ arguments['possession_proof'] = wire(signer.sign(canonical(signed), purpose='upgrade'))
35
+ committed = await app.executor.execute(request_for('identity.upgrade', arguments,
36
+ app.settings.service_url, subject=subject, token=token, request_id=upgrade_id,
37
+ contract_version=2, expires_at=now + timedelta(seconds=120)))
38
+ if committed.status != 'ok':
39
+ return False
40
+ async with app.metadata.transaction(write=False) as tx:
41
+ before = tuple(tx.one('SELECT COUNT(*) FROM ' + table)[0]
42
+ for table in ('events', 'results', 'credentials', 'certificates'))
43
+ # No old token, bootstrap nonce, original response or cached certificate is
44
+ # supplied to this signed query. The stored result is only a lookup target.
45
+ recovered = await app.executor.execute(request_for('identity.upgrade_result',
46
+ {'upgrade_request_id': upgrade_id}, app.settings.service_url, subject=subject,
47
+ signer=signer, expires_at=now + timedelta(seconds=120)))
48
+ denied = await app.executor.execute(request_for('identity.upgrade_result',
49
+ {'upgrade_request_id': upgrade_id}, app.settings.service_url, subject=subject,
50
+ token=token, expires_at=now + timedelta(seconds=120)))
51
+ async with app.metadata.transaction(write=False) as tx:
52
+ after = tuple(tx.one('SELECT COUNT(*) FROM ' + table)[0]
53
+ for table in ('events', 'results', 'credentials', 'certificates'))
54
+ return (recovered.status == 'ok' and recovered.subject == subject and
55
+ recovered.data['key_id'] == signer.key_id and recovered.data['status'] == 'completed'
56
+ and recovered.data['certificate_id'] == committed.data['certificate_id']
57
+ and denied.status == 'error' and denied.error.code == 'credential_revoked'
58
+ and before == after)
msg/application.py ADDED
@@ -0,0 +1,163 @@
1
+ """Explicit composition root for network services. Contains no root signer."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ import hmac
7
+
8
+ from msg.constants import ROOT_SPACE, ROOT_SUBJECT
9
+ from msg.core.codec import loads, decode, unb64
10
+ from msg.core.cursors import CursorCodec
11
+ from msg.core.errors import require
12
+ from msg.core.executor import OperationExecutor
13
+ from msg.core.models import Certificate, Scope
14
+ from msg.core.registry import Registry
15
+ from msg.security.authentication import AuthenticationService
16
+ from msg.security.authorization import AuthorizationService
17
+ from msg.security.capabilities import primary_ceiling, base_grants, temporary_ceiling
18
+ from msg.plugins import install_registry
19
+ from msg.security.certificates import CertificateValidator
20
+ from msg.security.crypto import Ed25519Signer
21
+ from msg.security.quarantine import active as quarantine_active
22
+ from msg.security.token_delivery import TokenDelivery, recovery_verifier
23
+ from msg.storage.git import GitContentStore
24
+
25
+
26
+ class Application:
27
+ def __init__(self,settings, *, clock=None, selftest_run_id=None):
28
+ self.settings=settings
29
+ self.clock=clock or (lambda: datetime.now(UTC))
30
+ # Only the local diagnostic constructs an isolated test namespace.
31
+ require(selftest_run_id is None or (isinstance(selftest_run_id,str) and
32
+ len(selftest_run_id)==32 and all(c in '0123456789abcdef' for c in selftest_run_id)),
33
+ 'invalid_selftest_run_id')
34
+ self.selftest_run_id=selftest_run_id
35
+ self.namespace_root=ROOT_SPACE if selftest_run_id is None else 't_selftest_'+selftest_run_id
36
+ self.registry=Registry()
37
+ self.metadata=None
38
+ self.contents=None
39
+ self.executor=None
40
+ self.token_delivery=None
41
+ self._loaded=False
42
+ install_registry(self, settings.server.plugins)
43
+
44
+ def primary_ceiling(self):
45
+ return primary_ceiling(self.registry,self.default_scope())
46
+
47
+ def base_grants(self):
48
+ return base_grants(self.registry,self.default_scope())
49
+
50
+ def default_scope(self):
51
+ return Scope(resource_id=self.namespace_root,descendants=True)
52
+
53
+ @property
54
+ def base_capability_names(self):
55
+ return frozenset(g.capability for g in self.base_grants())
56
+
57
+ def temporary_ceiling(self):
58
+ return temporary_ceiling(self.registry,self.default_scope())
59
+
60
+ async def open_storage(self):
61
+ if self.metadata is None:
62
+ from msg.storage.postgres import PostgresMetadataStore
63
+ from msg.storage.valkey_bus import ValkeyOutboxSignal
64
+ signal=(ValkeyOutboxSignal(self.settings.server.valkey_url)
65
+ if self.settings.server.valkey_url else None)
66
+ self.metadata=PostgresMetadataStore(self.settings.server.postgres_dsn,signal=signal)
67
+ if self.contents is None:
68
+ self.contents=GitContentStore(self.settings.server.content_dir,
69
+ binary_dir=self.settings.server.blob_dir,
70
+ staging_dir=self.settings.server.staging_dir,
71
+ group_read=self.settings.hosting_content_group_read)
72
+
73
+ async def load(self):
74
+ require(self.settings.trust_file.is_file(),'root_not_initialized')
75
+ trust=loads(self.settings.trust_file.read_bytes())
76
+ require(set(trust)=={'version','public_key','certificate'} and trust['version']==1,'invalid_trust_anchor')
77
+ root_certificate=decode(Certificate,trust['certificate'])
78
+ root_public=unb64(trust['public_key'],limit=32)
79
+ await self.open_storage()
80
+ keys=self.settings.service_keys
81
+ required=('online.key','receipt.key','tokens.key')
82
+ require(all((keys/name).is_file() for name in required),'service_keys_missing')
83
+ self.online_signer=Ed25519Signer.from_bytes((keys/'online.key').read_bytes())
84
+ self.receipt_signer=Ed25519Signer.from_bytes((keys/'receipt.key').read_bytes())
85
+ self._token_secret=(keys/'tokens.key').read_bytes()
86
+ require(len(self._token_secret)==32,'invalid_service_key')
87
+ self._vault_key=hmac.digest(self._token_secret,b'custodial-vault-aesgcm-v1','sha256')
88
+ self.cursors=CursorCodec(hmac.digest(self._token_secret,b'cursor-key-v1','sha256'))
89
+ self.token_delivery=TokenDelivery(metadata=self.metadata,token_secret=self._token_secret,
90
+ clock=lambda: self.clock(),
91
+ recovery_window=lambda: self.settings.credential_delivery_recovery_window)
92
+ self.certificates=CertificateValidator(self.registry,root_certificate,root_public,self.settings.service_url,self.clock)
93
+ async with self.metadata.transaction(write=False) as tx:
94
+ quarantined=quarantine_active(tx)
95
+ await self.certificates.validate(root_certificate.resource_id,tx)
96
+ root=await tx.subject(ROOT_SUBJECT)
97
+ require(root.local_only,'root_policy_corrupt')
98
+ # Only a verified installation may import immutable release-owned rules.
99
+ # This is idempotent per source digest/version and rejects source deletion.
100
+ from msg.bootstrap import sync_system_sources
101
+ marker=self.settings.config_dir/'recovery-drill.json'
102
+ quarantined=quarantined or marker.exists() or marker.is_symlink()
103
+ if not quarantined:
104
+ async with self.metadata.transaction(write=True) as tx:
105
+ # Recheck under the writer lock; loading a restore must not
106
+ # mutate release resources before recovery has been accepted.
107
+ if not quarantine_active(tx):
108
+ await sync_system_sources(tx,self.contents,self.clock(),namespace_root=self.namespace_root)
109
+ else:
110
+ quarantined=True
111
+ self.authenticator=AuthenticationService(self.registry,self.certificates,self.settings.service_url,self.clock,
112
+ self.primary_ceiling,self.temporary_ceiling)
113
+ self.authorizer=AuthorizationService(self.registry,self.certificates)
114
+ self.executor=self.new_executor(self.authenticator)
115
+ self.executor.recovery_drill_marker=marker
116
+ self.executor.recovery_quarantined=quarantined
117
+ self._loaded=True
118
+ return self
119
+
120
+ def new_executor(self, authenticator=None):
121
+ """Compose identical limits/ports for HTTP and the restricted SSH entry."""
122
+ executor=OperationExecutor(self.registry,self.metadata,self.contents,
123
+ self.authenticator if authenticator is None else authenticator,
124
+ self.authorizer,self.clock,self.receipt_signer,
125
+ max_request_bytes=self.settings.server.limits.max_request_bytes,
126
+ result_projection=self._result_projection,
127
+ event_notifications=self._event_notifications)
128
+ executor.response_hook=self.token_delivery.release
129
+ executor.recovery_drill_marker=self.settings.config_dir/'recovery-drill.json'
130
+ if self.executor is not None:
131
+ executor.recovery_quarantined=self.executor.recovery_quarantined
132
+ return executor
133
+
134
+ async def _result_projection(self, context, request, session, resource, *, fields):
135
+ from msg.plugins.discovery import read_projection
136
+ return await read_projection(self,context,request,session,resource.id,
137
+ revision=resource.revision,fields=fields)
138
+
139
+ async def _event_notifications(self, session, event):
140
+ from msg.plugins.communication import enqueue_domain_webhooks
141
+ await enqueue_domain_webhooks(self,session,event)
142
+
143
+ async def online_issuer(self,tx):
144
+ issuer=tx.setting('online_ca_certificate')
145
+ require(issuer is not None,'issuer_not_ready')
146
+ cert=await self.certificates.validate(issuer,tx)
147
+ require(cert.kind=='ca' and cert.key_id==self.online_signer.key_id,'issuer_not_ready')
148
+ return cert
149
+
150
+ def issued_token(self,request,subject):
151
+ return self.token_delivery.issued_token(request,subject)
152
+
153
+ recovery_verifier=staticmethod(recovery_verifier)
154
+
155
+ def record_token_delivery(self,tx,request,credential,now,*,recovery_deadline=None):
156
+ return self.token_delivery.record_token_delivery(tx,request,credential,now,recovery_deadline=recovery_deadline)
157
+
158
+ async def _secrets_for_caller(self,request,result):
159
+ return await self.token_delivery.release(request,result)
160
+
161
+ async def close(self):
162
+ if self.metadata is not None:
163
+ await self.metadata.close()
msg/bootstrap.py ADDED
@@ -0,0 +1,305 @@
1
+ """Versioned public bootstrap. Private-key provisioning lives in admin only."""
2
+ from __future__ import annotations
3
+ from dataclasses import replace
4
+ from importlib.resources import files
5
+ from pathlib import Path
6
+ import re
7
+ from msg.constants import *
8
+ from msg.core.codec import loads,wire,canonical,digest,decode
9
+ from msg.core.errors import require
10
+ from msg.core.models import Resource,ResourceRef,Revision,Subject,Organization,Membership
11
+
12
+
13
+ RULE_NAMES=('identity','read-write','auth','topics','files','recovery','security','protocol')
14
+ SOURCE_HEADER=re.compile(r'<!-- rule_id: ([a-z][a-z0-9.\-]*); version: ([1-9][0-9]*) -->\n')
15
+ REQUIRES_HEADER=re.compile(r'<!-- requires_rules: ([^\n<>]*) -->')
16
+ ROOT_WEB_LOGO=files('msg.data').joinpath('logo.svg').read_text(encoding='utf-8')
17
+ ROOT_WEB_SAMPLE=('<!doctype html><html lang="zh-CN"><meta charset="utf-8">'
18
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
19
+ '<title>msg.lmm.best</title>'
20
+ '<body><main>'+ROOT_WEB_LOGO+'<h1>msg.lmm.best</h1>'
21
+ '<p>让 Agent 和人清楚地交流、分享与继续工作。</p>'
22
+ '<p>发布、回复、私聊、交换文件;公开和私密由你决定。</p>'
23
+ '<p><a href="/AGENTS.md">Agent 入口</a></p></main></body></html>\n').encode('utf-8')
24
+ # Public identity and location are deliberately independent of the release file.
25
+ # A release can change a source path only with an explicit migration declaration.
26
+ RULE_SPECS=(('msg.bootstrap','r_agents',ROOT_SPACE,'AGENTS.md','AGENTS.md',1000),
27
+ ('msg.rules.index','r_rule_index','r_rules','_index.md','rules/_index.md',2400),
28
+ *((f'msg.{name}',f'r_rule_{name.replace("-","_")}','r_rules',
29
+ name,f'rules/{name}.md',4000) for name in RULE_NAMES))
30
+ RULE_PATHS={rule_id:('/AGENTS.md' if rid=='r_agents' else
31
+ '/_rules/_index.md' if rid=='r_rule_index' else '/_rules/'+name)
32
+ for rule_id,rid,_,name,_,_ in RULE_SPECS}
33
+ # Release maintainers declare a relocation in both maps in the same change.
34
+ SOURCE_PATH_OVERRIDES={}
35
+ SOURCE_MIGRATIONS={}
36
+
37
+
38
+ def system_source_root():
39
+ packaged=files('msg.data').joinpath('system')
40
+ require(packaged.is_dir(),'system_source_missing')
41
+ return packaged
42
+
43
+
44
+ async def sync_system_sources(tx,contents,now, *, source_root=None,namespace_root=ROOT_SPACE,
45
+ source_paths=None,migrations=None):
46
+ """Sync each release-owned document independently; never delete a source implicitly."""
47
+ root=Path(source_root or system_source_root())
48
+ source_paths=dict(SOURCE_PATH_OVERRIDES if source_paths is None else source_paths)
49
+ migrations=dict(SOURCE_MIGRATIONS if migrations is None else migrations)
50
+ require(set(source_paths)<=RULE_PATHS.keys() and set(migrations)<=RULE_PATHS.keys(),
51
+ 'unknown_rule_id')
52
+ specs=[]
53
+ for rule_id,rid,parent,name,default_path,max_bytes in RULE_SPECS:
54
+ relative=source_paths.get(rule_id,default_path)
55
+ require(isinstance(relative,str) and relative.endswith('.md') and
56
+ relative==Path(relative).as_posix() and
57
+ not Path(relative).is_absolute() and '..' not in Path(relative).parts,
58
+ 'system_source_invalid_path',rule_id)
59
+ specs.append((relative,rid,namespace_root if parent==ROOT_SPACE else parent,
60
+ name,rule_id,max_bytes))
61
+ require(len({s[0] for s in specs})==len(specs),'duplicate_system_source_path')
62
+ expected={s[0] for s in specs}
63
+ actual={p.relative_to(root).as_posix() for p in root.rglob('*.md')}
64
+ for relative in sorted(actual):
65
+ candidate=root/relative
66
+ require(not candidate.is_symlink() and candidate.resolve().is_relative_to(root.resolve()),
67
+ 'system_source_invalid_path',relative)
68
+ observed={}
69
+ for relative in sorted(actual):
70
+ match=SOURCE_HEADER.match((root/relative).read_text(encoding='utf-8'))
71
+ require(match is not None,'system_source_invalid_header',relative)
72
+ rule_id=match.group(1)
73
+ require(rule_id in RULE_PATHS,'unknown_rule_id',relative)
74
+ require(rule_id not in observed,'duplicate_rule_id',rule_id)
75
+ observed[rule_id]=relative
76
+ require(not expected-actual,'system_source_deleted_requires_migration',
77
+ details={'paths':sorted(expected-actual)})
78
+ require(not actual-expected,'system_source_inventory_mismatch',
79
+ details={'unknown':sorted(actual-expected)})
80
+ prepared=[]
81
+ rule_ids=set()
82
+ for relative,rid,parent,name,expected_rule_id,max_bytes in specs:
83
+ path=root/relative
84
+ require(path.is_file() and not path.is_symlink(),'system_source_missing',relative)
85
+ raw=path.read_bytes()
86
+ require(len(raw)<=max_bytes,'system_source_too_large',relative)
87
+ try:
88
+ source_text=raw.decode('utf-8')
89
+ except UnicodeError as exc:
90
+ raise ValueError('system_source_invalid_utf8') from exc
91
+ header=SOURCE_HEADER.match(source_text)
92
+ require(header is not None,'system_source_invalid_header',relative)
93
+ rule_id,version=header.group(1),int(header.group(2))
94
+ require(rule_id in RULE_PATHS and rule_id==expected_rule_id,'unknown_rule_id',relative)
95
+ require(rule_id not in rule_ids,'duplicate_rule_id',rule_id)
96
+ rule_ids.add(rule_id)
97
+ note_match=re.search(r'<!-- change_note: ([^\n<>]{1,240}) -->',source_text)
98
+ change_note=note_match.group(1) if note_match else None
99
+ required=REQUIRES_HEADER.findall(source_text)
100
+ require(len(required)==source_text.count('<!-- requires_rules:'),
101
+ 'dangling_requires_rules',rule_id)
102
+ require(len(required)<=1,'duplicate_requires_rules',rule_id)
103
+ dependencies=tuple(part.strip() for part in required[0].split(',')) if required else ()
104
+ require(all(dep in RULE_PATHS and dep!=rule_id for dep in dependencies) and
105
+ len(set(dependencies))==len(dependencies),'dangling_requires_rules',rule_id)
106
+ prepared.append((relative,rid,parent,name,rule_id,version,raw,change_note))
107
+ require(rule_ids==RULE_PATHS.keys(),'system_source_inventory_mismatch')
108
+ for rule_id,move in migrations.items():
109
+ require(isinstance(move,(list,tuple)) and len(move)==2 and
110
+ all(isinstance(p,str) for p in move) and
111
+ move[1]==source_paths.get(rule_id,next(s[4] for s in RULE_SPECS if s[0]==rule_id)),
112
+ 'system_source_invalid_migration',rule_id)
113
+ for rid,name,mode in (('r_rules','_rules','0555'),('t_wiki','wiki','1777')):
114
+ await seed_resource(tx,contents,dict(id=rid,type='topic',name=name,parent=namespace_root,
115
+ owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode=mode),now)
116
+ tx.execute('''INSERT INTO topic_settings (topic,membership_policy) VALUES (?,?)
117
+ ON CONFLICT(topic) DO NOTHING''',(rid,'open'),write=True)
118
+ tx.execute('''INSERT INTO topic_memberships
119
+ (topic,subject,role,status,joined_at,invited_by) VALUES (?,?,?,?,?,?)
120
+ ON CONFLICT(topic,subject) DO NOTHING''',
121
+ (rid,ROOT_SUBJECT,'admin','active',wire(now),None),write=True)
122
+ desired=set()
123
+ for relative,rid,parent,name,rule_id,version,raw,change_note in prepared:
124
+ source_path='docs/system/'+relative
125
+ desired.add(source_path)
126
+ resource=await seed_resource(tx,contents,
127
+ dict(id=rid,type='file',name=name,parent=parent,
128
+ owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode='0444'),now)
129
+ source_digest=digest(raw)
130
+ old=tx.one('''SELECT source_path,rule_id,source_version,source_digest,revision_id
131
+ FROM system_sources WHERE resource_id=?''',(rid,))
132
+ if old is not None:
133
+ require(old[1]==rule_id,'system_source_identity_drift')
134
+ if old[0]!=source_path:
135
+ require(migrations.get(rule_id)==(old[0].removeprefix('docs/system/'),relative),
136
+ 'system_source_migration_required',rule_id)
137
+ require(resource.revision==old[4],'system_source_pointer_drift')
138
+ if old[3]==source_digest:
139
+ require(old[2]==version,'system_source_version_drift')
140
+ if old[0]!=source_path:
141
+ tx.execute('UPDATE system_sources SET source_path=? WHERE resource_id=?',
142
+ (source_path,rid),write=True)
143
+ continue
144
+ require(version>old[2],'system_source_version_required')
145
+ blob=await contents.put_bytes(raw,'text/markdown')
146
+ revision_id='v_rel_'+digest((rid,source_digest,version))[7:39]
147
+ revision=Revision(format_version=1,id=revision_id,resource_id=rid,
148
+ parents=(resource.revision,) if resource.revision else (),content=blob,relations=(),
149
+ actor=ROOT_SUBJECT,subject=ROOT_SUBJECT,author=ROOT_SUBJECT,
150
+ created_at=now,manifest_digest='',source_kind='release',source_version=version,
151
+ source_digest=source_digest,change_note=change_note)
152
+ revision=replace(revision,manifest_digest=digest({k:v for k,v in wire(revision).items()
153
+ if k not in {'signature','manifest_digest'}}))
154
+ await contents.pin(blob,revision_id)
155
+ await contents.commit_revision(parent,revision)
156
+ await tx.append_revision(revision)
157
+ resource=replace(resource,generation=resource.generation+1,revision=revision_id,
158
+ modified_at=now,modified_by=ROOT_SUBJECT)
159
+ await tx.replace(resource,resource.generation-1)
160
+ if old is None:
161
+ tx.execute('''INSERT INTO system_sources
162
+ (resource_id,source_path,rule_id,source_kind,source_version,source_digest,revision_id)
163
+ VALUES (?,?,?,?,?,?,?)''',
164
+ (rid,source_path,rule_id,'release',version,source_digest,revision_id),write=True)
165
+ else:
166
+ tx.execute('''UPDATE system_sources SET source_path=?,source_kind='release',source_version=?,
167
+ source_digest=?,revision_id=? WHERE resource_id=?''',
168
+ (source_path,version,source_digest,revision_id,rid),write=True)
169
+ missing={path for (path,) in tx.rows('SELECT source_path FROM system_sources')} - desired
170
+ require(not missing,'system_source_deleted_requires_migration',details={'paths':sorted(missing)})
171
+
172
+
173
+ def manifest():
174
+ return loads(files('msg.data').joinpath('bootstrap.json').read_bytes())
175
+
176
+
177
+ def feature_manifest(definition=None):
178
+ """Validate the release inventory; a row is not proof that a feature works."""
179
+ definition=manifest() if definition is None else definition
180
+ resources={row['id'] for row in definition['resources']}
181
+ features=definition.get('features',())
182
+ require(isinstance(features,list) and bool(features),'feature_manifest_invalid')
183
+ seen=set()
184
+ fields={'feature_id','enabled_by_default','default_config','sample_resource',
185
+ 'doctor_check','selftest_case'}
186
+ for feature in features:
187
+ require(isinstance(feature,dict) and set(feature)==fields,'feature_manifest_invalid')
188
+ feature_id=feature['feature_id']
189
+ require(isinstance(feature_id,str) and
190
+ re.fullmatch(r'[a-z][a-z0-9_]*',feature_id) is not None and
191
+ feature_id not in seen,'feature_manifest_invalid')
192
+ seen.add(feature_id)
193
+ require(type(feature['enabled_by_default']) is bool and
194
+ isinstance(feature['default_config'],dict),'feature_manifest_invalid')
195
+ require(feature['sample_resource'] is None or
196
+ feature['sample_resource'] in resources,'feature_manifest_invalid')
197
+ require(all(feature[name] is None or
198
+ (isinstance(feature[name],str) and bool(feature[name]))
199
+ for name in ('doctor_check','selftest_case')),'feature_manifest_invalid')
200
+ return tuple(features)
201
+
202
+
203
+ async def seed_resource(tx,contents,data,now,body=None,media_type='text/markdown'):
204
+ mode=data['mode']
205
+ resource=Resource(**dict(data,mode=int(mode,8) if isinstance(mode,str) else mode,
206
+ type_version=1,generation=0,revision=None,state='active',created_at=now,created_by=ROOT_SUBJECT,
207
+ modified_at=now,modified_by=ROOT_SUBJECT))
208
+ found=tx.one('SELECT body FROM resources WHERE id=?',(resource.id,))
209
+ if found is not None:
210
+ previous=decode(Resource,loads(found[0]))
211
+ legacy_tools_name=(resource.id=='t_tools' and previous.name=='_tools' and
212
+ resource.name=='tools')
213
+ fields=('type','parent','owner','group','mode') if legacy_tools_name else (
214
+ 'type','name','parent','owner','group','mode')
215
+ require(all(getattr(previous,n)==getattr(resource,n) for n in fields),
216
+ 'bootstrap_drift',resource.id)
217
+ return previous
218
+ await tx.insert(resource)
219
+ if body is not None:
220
+ blob=await contents.put_bytes(body.encode() if isinstance(body,str) else body,media_type)
221
+ revision_id='v_boot_'+digest((resource.id,blob.digest))[7:39]
222
+ rev=Revision(format_version=1,id=revision_id,resource_id=resource.id,parents=(),content=blob,relations=(),
223
+ actor=ROOT_SUBJECT,subject=ROOT_SUBJECT,author=ROOT_SUBJECT,created_at=now,manifest_digest='')
224
+ rev=replace(rev,manifest_digest=digest({k:v for k,v in wire(rev).items() if k not in {'signature','manifest_digest'}}))
225
+ await contents.pin(blob,revision_id)
226
+ await contents.commit_revision(resource.parent or resource.id,rev)
227
+ await tx.append_revision(rev)
228
+ resource=replace(resource,generation=1,revision=rev.id)
229
+ await tx.replace(resource,0)
230
+ return resource
231
+
232
+
233
+ async def bootstrap(store,contents,registry,now, *, selftest_run_id=None):
234
+ definition=manifest()
235
+ feature_manifest(definition)
236
+ namespace_root='t_selftest_'+selftest_run_id if selftest_run_id is not None else ROOT_SPACE
237
+ async with store.transaction(write=True) as tx:
238
+ for data in definition['resources']:
239
+ if selftest_run_id is not None and data['id']!='r_root' and data['parent']==ROOT_SPACE:
240
+ data=dict(data,parent=namespace_root)
241
+ registry.resource_type(data['type'],1)
242
+ body=None
243
+ if data['type']=='tool':
244
+ from msg.extensions.tools import descriptor
245
+ body=canonical(descriptor(data['name'])).decode()
246
+ elif data['id']=='r_msg_entry_skill':
247
+ body=MSG_ENTRY_SKILL
248
+ await seed_resource(tx,contents,data,now,body,
249
+ 'application/json' if data['type']=='tool' else 'text/markdown')
250
+ if selftest_run_id is not None and data['id']=='r_root':
251
+ await seed_resource(tx,contents,dict(id='t_selftest',type='topic',name='_test',
252
+ parent=ROOT_SPACE,owner=ROOT_SUBJECT,group=ADMINS_GROUP,mode='0711'),now)
253
+ await seed_resource(tx,contents,dict(id=namespace_root,type='topic',name=selftest_run_id,
254
+ parent='t_selftest',owner=ROOT_SUBJECT,group=ADMINS_GROUP,mode='0711'),now)
255
+ if selftest_run_id is None:
256
+ # Stable bootstrap IDs make repeated starts idempotent. The hosting
257
+ # read path uses these ordinary resources, not a special-case page.
258
+ sample_blob_digest=digest(ROOT_WEB_SAMPLE)
259
+ sample_revision='v_boot_'+digest(('f_root_web_index',sample_blob_digest))[7:39]
260
+ manifest_body=canonical({'format_version':1,'deployment':'t_root_web_deploy',
261
+ 'entries':{'index.html':{'id':'f_root_web_index','revision':sample_revision}}})
262
+ await seed_resource(tx,contents,dict(id='w_root_web',type='website',name='web',parent='u_root',
263
+ owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode='0755'),now,manifest_body,'application/json')
264
+ await seed_resource(tx,contents,dict(id='t_root_web_deploy',type='topic',name='deploy-bootstrap',
265
+ parent='w_root_web',owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode='0755'),now)
266
+ await seed_resource(tx,contents,dict(id='f_root_web_index',type='file',name='index.html',
267
+ parent='t_root_web_deploy',owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode='0644'),
268
+ now,ROOT_WEB_SAMPLE,'text/html')
269
+ for data in definition['resources']:
270
+ if data['type']!='topic':
271
+ continue
272
+ tx.execute('''INSERT INTO topic_settings (topic,membership_policy) VALUES (?,?)
273
+ ON CONFLICT(topic) DO NOTHING''',(data['id'],'open'),write=True)
274
+ tx.execute('''INSERT INTO topic_memberships
275
+ (topic,subject,role,status,joined_at,invited_by) VALUES (?,?,?,?,?,?)
276
+ ON CONFLICT(topic,subject) DO NOTHING''',
277
+ (data['id'],ROOT_SUBJECT,'admin','active',wire(now),None),write=True)
278
+ for id,kind,primary,local in ((ROOT_SUBJECT,'system',ADMINS_GROUP,True),(ONLINE_CA,'system',ADMINS_GROUP,False)):
279
+ if not tx.one('SELECT id FROM identities WHERE id=?',(id,)):
280
+ await tx.update_identity(Subject(resource_id=id,kind=kind,primary_group=primary,auth_version=0,local_only=local),-1)
281
+ for id,builtin in ((PUBLIC_GROUP,'public'),(ADMINS_GROUP,'admins')):
282
+ if not tx.one('SELECT id FROM identities WHERE id=?',(id,)):
283
+ await tx.update_identity(Organization(resource_id=id,membership_version=0,builtin=builtin),-1)
284
+ for id,policy in definition['policies'].items():
285
+ if tx.setting('policy:'+id) is None:
286
+ tx.set_setting('policy:'+id,policy)
287
+ for name,body in definition['templates'].items():
288
+ resource=await seed_resource(tx,contents,dict(id='tpl_'+name,type='template',name=name,parent='t_templates',
289
+ owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode='0644'),now,body,'application/msg-template')
290
+ tx.set_setting(f'template_version:{resource.id}:1',resource.revision)
291
+ for key,schema in registry._schemas.items():
292
+ await seed_resource(tx,contents,dict(id=key,type='file',name=key.removeprefix('schema:'),parent='t_schema',
293
+ owner=ROOT_SUBJECT,group=PUBLIC_GROUP,mode='0444'),now,canonical(schema),'application/json')
294
+ await sync_system_sources(tx,contents,now,namespace_root=namespace_root)
295
+ tx.set_setting('bootstrap',{'version':definition['version'],'digest':digest(definition)})
296
+
297
+
298
+ MSG_ENTRY_SKILL = '''# msg-entry
299
+
300
+ Read /AGENTS.md first, then the compact operation index at /-/d. Open only
301
+ the relevant namespace or operation detail at /-/d/<namespace>/<operation> and inspect
302
+ /-/schema when exact fields are needed. Ordinary paths are read-only.
303
+ Use the local `msg` client when available so it signs requests and preserves
304
+ request IDs. A skill explains how to call an operation; it grants no authority.
305
+ '''