fix: keep global HTTP off the event loop

WhatToChange: recommended-changes/2026-09-21-event-loop-and-ledger-perms/
  What-to-Change.md
  What-to-Change-To.md
  How-to-implement-the-changes.md

What was actually done:
- Global HTTP stamp and verify run via asyncio.to_thread.
- File ledger saves chmod 0600.
- .gitignore covers coverage, mypy, logs, and editor directories.

What was not done:
- (none)
This commit is contained in:
George Lambert 2026-09-21 14:11:29 -04:00
commit 60bcefbca2
10 changed files with 100 additions and 2 deletions

9
.gitignore vendored
View file

@ -4,8 +4,17 @@ __pycache__/
*.pyc
.ruff_cache/
.pytest_cache/
.mypy_cache/
.dmypy.json
.coverage
.coverage.*
htmlcov/
dist/
build/
*.egg-info/
.env
.env.*
*.log
*.tmp
.idea/
.vscode/

View file

@ -0,0 +1,11 @@
# Done and not done
WhatToChange: recommended-changes/2026-09-21-event-loop-and-ledger-perms/
What was actually done:
- Global HTTP stamp and verify run via asyncio.to_thread.
- File ledger saves chmod 0600.
- .gitignore covers coverage, mypy, logs, and editor directories.
What was not done:
- (none)

View file

@ -0,0 +1,8 @@
# How to implement the changes:
Diffs are against `main` @ `1bbd0a6` with a dirty tree.
1. `src/verae_mcp_timestamping_server/global_client.py` — `await asyncio.to_thread(...)` around `submit` and `verify_certificate`.
2. `src/verae_mcp_timestamping_server/ledger.py` — `os.chmod(self.path, 0o600)` after `_save`.
3. `.gitignore` — add `.coverage`, `htmlcov/`, `.mypy_cache/`, `*.log`, `.idea/`, `.vscode/`.
4. `uv run pytest`.

View file

@ -0,0 +1,26 @@
# Recommended changes: event loop and ledger permissions
Review of the Verae MCP timestamping server on 2026-09-21. Pins are in git-versions.md.
## Install
```sh
cd /Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server
uv sync --group dev
```
## Usage
```sh
less recommended-changes/2026-09-21-event-loop-and-ledger-perms/What-to-Change.md
less recommended-changes/2026-09-21-event-loop-and-ledger-perms/What-to-Change-To.md
less recommended-changes/2026-09-21-event-loop-and-ledger-perms/How-to-implement-the-changes.md
```
## Contributing
Questions: this research workspace. PRs welcome.
## License
UNLICENSED © George Lambert

View file

@ -0,0 +1,7 @@
# What to Change To:
| Component | Target behavior | Acceptance criteria | Constraints |
| --- | --- | --- | --- |
| Global HTTP calls | `submit` and `verify_certificate` run in a worker thread | `uv run pytest` still passes, including `test_global_client.py` | Same JSON bodies and receipt fields |
| Local receipt file | Mode `0600` after every save | New file is not group- or world-readable | Do not change the JSON shape |
| `.gitignore` | Ignore coverage, mypy, logs, IDE dirs | `git status` does not list those artifacts | Keep `debug-trace/` tracked; those HTML traces are the stamp record |

View file

@ -0,0 +1,16 @@
# What to Change:
Reviewed against `main` @ `1bbd0a6` (dirty working tree). `uv run pytest` — 14 passed.
## What is working
- Tools `stamp`, `verify`, and `hash_report` accept a SHA-256 hex digest and refuse to take file bytes.
- `scope=global` posts to `https://api.veraetime.net/api/timestamp` with `wait=true`.
- Local receipts persist in `~/.verae/mcp-timestamp-local.json`.
- Job-complete traffic is not on JetStream. NATS use here is request/reply on `verae.ts.request`, `verae.ts.verify`, and `verae.hash.report` only when a NatsLedger is constructed. The default process uses the file ledger plus HTTPS.
| Component | Problem | Evidence | Severity | Type |
| --- | --- | --- | --- | --- |
| `GlobalLedger.stamp` / `verify` | Blocking `urllib` and `time.sleep` run on the asyncio loop while other MCP tools wait | `global_client.py` `submit` calls `urlopen` and `time.sleep(0.25)` from `async def stamp` | high | design flaw |
| `FileLedger._save` | Receipt file is created with the process umask, often world-readable | `ledger.py` `write_text` with no `chmod` | high | missing gate |
| `.gitignore` | Coverage, mypy, logs, and editor dirs are not ignored | `.gitignore` listed venv and pytest only | low | missing gate |

View file

@ -0,0 +1,12 @@
[
{
"path": "/Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server",
"remote": "",
"branch": "main",
"head": "1bbd0a68643e51c21bb8975e01b7f5cfc0df3a14",
"short": "1bbd0a6",
"dirty": true,
"describe": "1bbd0a6-dirty",
"porcelain": "M .gitignore\n M src/verae_mcp_timestamping_server/global_client.py\n M src/verae_mcp_timestamping_server/ledger.py"
}
]

View file

@ -0,0 +1,6 @@
# git-versions
Pins at pack creation. Dirty means uncommitted files existed.
- `/Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server` `main` `1bbd0a6` (dirty) `1bbd0a6-dirty`
- HEAD: `1bbd0a68643e51c21bb8975e01b7f5cfc0df3a14`

View file

@ -6,6 +6,7 @@ The global server is the source of truth for scope=global.
from __future__ import annotations
import asyncio
import json
import os
import time
@ -225,11 +226,12 @@ class GlobalLedger:
) -> list[Receipt]:
del org_id
digest = validate_sha256(sha256)
return [self.client.submit(digest, cube_id=cube_id)]
receipt = await asyncio.to_thread(self.client.submit, digest, cube_id)
return [receipt]
async def verify(self, receipt_id: str, want_scope: str = "") -> VerifyResult:
del want_scope
return self.client.verify_certificate(receipt_id)
return await asyncio.to_thread(self.client.verify_certificate, receipt_id)
async def hash_report(
self, sha256: str, org_id: str = "", want_scope: str = ""

View file

@ -184,6 +184,7 @@ class FileLedger(MemoryLedger):
self.path.parent.mkdir(parents=True, exist_ok=True)
payload = [self._by_id[rid].model_dump() for rid in self._order]
self.path.write_text(json.dumps(payload, indent=2) + "\n")
os.chmod(self.path, 0o600)
async def stamp(
self,