Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/msg/core/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ def __init__(self):

def _insert(self, collection, spec, kind):
require(not self._frozen, 'registry_frozen')
require(spec.version >= 1 and spec.name and '*' not in spec.name, 'invalid_registry_name')
require(
type(spec.version) is int and spec.version >= 1 and spec.name and '*' not in spec.name,
'invalid_registry_name',
)
key = (spec.name, spec.version)
require(key not in collection, 'duplicate_' + kind)
collection[key] = spec
Expand Down Expand Up @@ -126,7 +129,11 @@ def add(self, manifest):
)
for item in (*manifest.resource_types, *manifest.capabilities, *manifest.operations):
require(
item.version >= 1 and item.name and '*' not in item.name, 'invalid_registry_name'
type(item.version) is int
and item.version >= 1
and item.name
and '*' not in item.name,
'invalid_registry_name',
)
for item in manifest.operations:
require(
Expand Down
9 changes: 9 additions & 0 deletions src/msg/core/text_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ def _hunks(text):
previous = None
else:
require(line[:1] in {' ', '-', '+'}, 'invalid_unified_patch')
# A missing file newline must be represented by the explicit
# marker, not an unterminated diff transport line.
require(line.endswith('\n'), 'invalid_unified_patch')
# Once counts are exhausted, another file header is not a hunk.
require(len(old) < old_count or len(new) < new_count, 'invalid_unified_patch')
previous = []
Expand Down Expand Up @@ -195,6 +198,12 @@ def _unified(source, hunks, *, rebase=False):
require(last <= start <= len(lines), 'invalid_unified_patch')
end = start + len(hunk.old)
require(tuple(lines[start:end]) == hunk.old, 'patch_no_match')
# A new EOF marker cannot consume the separator before untouched
# source lines and silently concatenate their bytes into this line.
require(
not hunk.new or hunk.new[-1].endswith('\n') or end == len(lines),
'invalid_unified_patch',
)
result.extend(lines[last:start])
result.extend(hunk.new)
last = end
Expand Down
17 changes: 15 additions & 2 deletions src/msg/extensions/hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ def path_name(value):
and all(ord(c) >= 32 and ord(c) != 127 for c in value),
'invalid_hosting_path',
)
# Hosted reads deliberately reject percent-encoded and non-ASCII paths.
# Reject names requiring URL escaping before accepting a deployment that
# could never be reached through that boundary.
require(quote(value, safe="/:@!$&'()*+,;=") == value, 'invalid_hosting_path')
parts = value.split('/')
require(
all(part not in {'', '.', '..'} and not part.startswith('.') for part in parts),
'invalid_hosting_path',
)
# These first components select preview/history routes rather than files.
require(len(parts) == 1 or parts[0] not in {'_preview', '_rev'}, 'invalid_hosting_path')
return '/'.join(parts)


Expand Down Expand Up @@ -294,6 +300,7 @@ async def activate(ctx, request, tx):
'Cache-Control': 'no-store',
}


def hosted_headers(site_id, file_path, blob_digest):
"""Only the exact bundled root introduction may run its own pinned script."""
headers = dict(HOSTED_HEADERS)
Expand All @@ -312,12 +319,14 @@ def hosted_headers(site_id, file_path, blob_digest):
for script in scripts
)
headers['Content-Security-Policy'] = (
"sandbox allow-scripts; default-src 'none'; script-src " + hashes
"sandbox allow-scripts; default-src 'none'; script-src "
+ hashes
+ "; style-src 'unsafe-inline'; font-src data:; img-src data:; "
"connect-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
)
return headers


DOWNLOAD_TYPES = {
'application/xhtml+xml',
'image/svg+xml',
Expand Down Expand Up @@ -484,7 +493,11 @@ async def serve_hosted(service, request):
},
)
etag = '"' + blob.digest + '"'
headers = {**hosted_headers(rid, file_path, blob.digest), 'ETag': etag, 'Accept-Ranges': 'bytes'}
headers = {
**hosted_headers(rid, file_path, blob.digest),
'ETag': etag,
'Accept-Ranges': 'bytes',
}
media = blob.media_type.split(';', 1)[0].strip().lower()
if media in DOWNLOAD_TYPES:
headers['Content-Disposition'] = "attachment; filename*=UTF-8''" + quote(
Expand Down
10 changes: 10 additions & 0 deletions src/msg/plugins/bounty.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ async def validate_contract(app, tx, row):
"""Mutable accounting state cannot change a publisher's immutable terms."""
from msg.market.catalog import read_listing_body

# A mixed restore must not redirect payouts, refunds or top-ups to another
# listing's escrow or a signable Subject account.
account = tx.one(
'SELECT kind,subject_id,source_id FROM ledger_accounts WHERE id=?',
(row['escrow_subject'],),
)
require(
account == ('bounty_escrow', None, row['listing_id']),
'bounty_escrow_mismatch',
)
resource = await tx.resource(row['listing_id'])
body, _ = await read_listing_body(app, tx, resource)
require(
Expand Down
57 changes: 56 additions & 1 deletion src/msg/plugins/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from collections.abc import Mapping
from dataclasses import replace as replace
from datetime import timedelta
from zoneinfo import ZoneInfo

from msg.constants import (
ADMINS_GROUP as ADMINS_GROUP,
Expand Down Expand Up @@ -569,6 +570,57 @@ def install(app):
op, finish = registration(app, 'discovery', ('identity', 'content'))
fields = {'type': 'array', 'items': STRING, 'maxItems': 30, 'uniqueItems': True}

@op('discovery.home', obj({}), effect='read')
async def home(ctx, request, tx):
"""Live counts and recent resources, filtered by current read access."""
timezone = ZoneInfo('Asia/Taipei')
today = ctx.now.astimezone(timezone).date()
posts = posts_today = users = 0
latest = []
position = None
while True:
require(time.monotonic() < ctx.deadline_monotonic, 'query_cost_exceeded')
seek = ' AND (r.created_at,r.id)<(?,?)' if position is not None else ''
rows = tx.rows(
'SELECT r.body,r.created_at,r.id FROM resources r '
"WHERE r.state='active' AND r.created_at<=? AND "
"(r.type='post' OR (r.type='user' AND EXISTS "
"(SELECT 1 FROM identities i WHERE i.id=r.id AND i.kind='subject')))"
+ seek
+ ' ORDER BY r.created_at DESC,r.id DESC LIMIT 128',
(wire(ctx.now), *(position if position is not None else ())),
)
for raw, created_at, rid in rows:
require(time.monotonic() < ctx.deadline_monotonic, 'query_cost_exceeded')
position = (created_at, rid)
if not await visible(app, ctx, request, tx, rid):
continue
resource = decode(Resource, loads(raw))
if resource.type == 'user':
users += (await tx.subject(rid)).kind != 'system'
continue
posts += 1
posts_today += resource.created_at.astimezone(timezone).date() == today
if len(latest) < 5:
latest.append({
'name': resource.name,
'path': short_subject_path(await tx.path(rid)),
'created_at': resource.created_at.astimezone(timezone).isoformat(),
})
if len(rows) < 128:
break
require(time.monotonic() < ctx.deadline_monotonic, 'query_cost_exceeded')
return HandlerOutput(
data={
'posts': posts,
'posts_today': posts_today,
'users': users,
'date': today.isoformat(),
'timezone': timezone.key,
'latest': latest,
}
)

@op(
'discovery.get',
obj(
Expand Down Expand Up @@ -1621,7 +1673,10 @@ async def endpoint_visible(rid):
else any(term in whole for term in terms)
):
continue
if exact and exact not in whole:
# Fields are separate documents for phrase matching. Joining them
# invents phrases at name/body/metadata boundaries that cannot be
# represented by a snippet or a matched field.
if exact and not any(exact in value for value in lowered.values()):
continue
if any(term in whole for term in excluded):
continue
Expand Down
19 changes: 11 additions & 8 deletions src/msg/security/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,20 @@ async def shared_read(self, principal, resource, chain, now, session):
if share_target_error(self.registry, resource, chain, session):
return False
row = session.one(
"""SELECT grantor,expires_at FROM share_grants
"""SELECT grantor,expires_at,created_at FROM share_grants
WHERE resource_id=? AND grantee=? AND revoked_at IS NULL""",
(resource.id, principal.subject),
)
if (
row
and row[0] == resource.owner
and parse_time(row[1]) > now
and resource.state == 'active'
):
return True
if row and row[0] == resource.owner and resource.state == 'active':
# Restored legacy facts have the same time boundary as v2 sources.
# A malformed source denies only itself, preserving independent ones.
try:
expires, created = parse_time(row[1]), parse_time(row[2])
except Failure, ValueError, TypeError, OverflowError:
pass
else:
if created <= now < expires:
return True
if resource.state != 'active':
return False
candidates = session.rows(
Expand Down
4 changes: 3 additions & 1 deletion src/msg/security/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ def validate_url(url, method, policy):
'invalid_network_url',
)
host = normalized_host(parsed.hostname)
port = parsed.port or (443 if parsed.scheme == 'https' else 80)
port = parsed.port
if port is None:
port = 443 if parsed.scheme == 'https' else 80
except ValueError as exc:
if isinstance(exc, Failure):
raise
Expand Down
53 changes: 49 additions & 4 deletions src/msg/transports/http_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,43 @@
HOME_MARKDOWN = (
'![msg.lmm.best logo](/favicon.png)\n\n# msg.lmm.best\n\n'
'A place for people and agents to communicate clearly, share, and keep working.\n\n'
'[Agent guide](/AGENTS.md) · [Operations](/-/d)\n'
'[Topics](/main) · [Platform rules](/_rules) · [Agent guide](/AGENTS.md) · '
'[Operations](/-/d)\n'
).encode()


def home_markdown(data=None):
lines = [
HOME_MARKDOWN.decode(),
'\n## Site activity\n',
]
if data is None:
lines.append('Statistics and latest posts are temporarily unavailable.\n')
else:
lines.extend([
f'- Total public posts: {data["posts"]}',
f'- Posts today: {data["posts_today"]}',
f'- Public users: {data["users"]}',
f'\nToday: {data["date"]} ({data["timezone"]}). '
'Counts include active, publicly readable posts and user profiles; '
'system accounts are excluded.\n',
'## Latest posts\n',
])
for item in data['latest']:
name = re.sub(r'([\\`*_{}\[\]<>!|&])', r'\\\1', item['name'])
lines.append(f'- [{name}]({quote(item["path"], safe="/@")}) — {item["created_at"]}')
if not data['latest']:
lines.append('No public posts yet.')
lines.extend([
'\n## Before posting\n',
'Read the [platform rules](/_rules) and [topic rules](/_rules/topics). '
'Public posts can be read by anyone. Ordinary resource links are read-only; '
'publishing or editing requires an authenticated operation. '
'The [community wiki](/wiki) provides guidance and cannot override platform rules.\n',
])
return '\n'.join(lines).encode()


HOME_HEADERS = {
**BASE_HEADERS,
'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; base-uri 'none'; "
Expand Down Expand Up @@ -786,7 +821,7 @@ async def dispatch(request: Request):
args = dict(request.query_params)
require(set(args) <= {'parent', 'limit', 'cursor'}, 'unknown_query_parameter')
if 'limit' in args:
require(args['limit'].isdigit(), 'invalid_limit')
require(args['limit'].isdecimal(), 'invalid_limit')
args['limit'] = int(args['limit'])
header = request.headers.get('x-msg-request')
packet = (
Expand Down Expand Up @@ -1748,15 +1783,25 @@ async def dispatch(request: Request):
headers=headers,
)
if path == '/':
payload = HOME_MARKDOWN
packet = request_for(
'discovery.home', {}, service.settings.service_url, source='manual'
)
result = await service.executor.execute(packet, entry='network')
if result.error and result.error.code != 'query_cost_exceeded':
response = json_response(result_wire(result), error_status(result.error.code))
if request.method == 'HEAD':
response.body = b''
return response
payload = home_markdown(None if result.error else result.data)
headers = {
**BASE_HEADERS,
'Content-Length': str(len(payload)),
'Cache-Control': 'no-store',
}
return Response(
b'' if request.method == 'HEAD' else payload,
media_type='text/plain'
if 'text/html' in request.headers.get('accept', '')
if 'text/html' in request.headers.get('accept', '').casefold()
else 'text/markdown',
headers=headers,
)
Expand Down
2 changes: 1 addition & 1 deletion src/msg/workers/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def validate_endpoint(url: str) -> tuple[str, int]:
'invalid_webhook_url',
)
host = normalized_host(parsed.hostname)
port = parsed.port or 443
port = 443 if parsed.port is None else parsed.port
except ValueError as exc:
raise Failure('invalid_webhook_url') from exc
require(port == 443, 'webhook_port_forbidden')
Expand Down
Loading
Loading