docs: embedded NATS, bearer require flag, host-token keeper

Amend design/plan: in-process NATS calling VeraeTime; MCP bearer
passthrough when present; VERAE_MCP_REQUIRE_BEARER=0 keeps a server
host token when none supplied; =1 requires caller bearer.
This commit is contained in:
George Lambert 2026-09-29 11:49:43 -04:00
commit 6c20d87131
2 changed files with 74 additions and 38 deletions

View file

@ -18,6 +18,8 @@
- No JetStream (or other persistent stream) for job-complete / jobId notification.
- MCP tools: `stamp`, `verify`, `hash_report`, `signup`, `login`, `session`, `create_user`.
- `pkg/tsnats` subjects: `verae.ts.request`, `verae.ts.verify`, `verae.hash.report`; subscribe `verae.ts.completed`.
- Embedded NATS in-process (`VERAE_NATS_LISTEN`, default `127.0.0.1:4223`); handlers call VeraeTime/FileLedger.
- `VERAE_MCP_REQUIRE_BEARER` default `0`: host-token keeper when no caller bearer; `1` requires caller bearer.
- Shadow default port `8001`; Python keeps `8000`.
- Work in `/Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server-go`.
- READMEs follow Standard Readme when touched.
@ -143,21 +145,24 @@ git commit -m "feat: pkg/tsnats digests-only NATS timestamp client"
---
### Task 3: VeraeTime HTTPS client + auth.properties
### Task 3: VeraeTime HTTPS client + auth.properties + host-token keeper
**Files:**
- Create: `internal/veraetime/client.go`, `authprops.go`, `*_test.go`
- Create: `internal/veraetime/client.go`, `authprops.go`, `keeper.go`, `*_test.go`
- Create: `internal/authctx/resolve.go` (request bearer vs host token per `RequireBearer`)
**Interfaces:**
- Produces: `Stamp(digest)`, `Verify(receiptID)`, `HashReport(digest)`, `Signup`, `Login`, `Session`, `CreateUser`
- Token order: ctor → `VERAE_GLOBAL_TOKEN` → auth.properties
- Produces: `Stamp(digest, bearer)`, `Verify`, `HashReport`, `Signup`, `Login`, `Session`, `CreateUser`
- `Keeper.Token(ctx) (string, error)` — refresh via `/auth/login` when missing/near expiry; or `VERAE_GLOBAL_TOKEN`
- `authctx.Resolve(reqBearer string, requireBearer bool, k *Keeper) (string, error)`
- Stamp body: `data`, `digest`, `hashAlg: SHA256`, `restamp: false`
- Never log tokens
- [ ] **Step 1: Fake HTTP round-tripper tests** (existing certificate path + 401 → AuthError)
- [ ] **Step 1: Fake HTTP tests** (existing cert path; 401 AuthError; keeper refresh; require-bearer rejects empty)
- [ ] **Step 2: Fail**
- [ ] **Step 3: Implement**
- [ ] **Step 4: Pass** — `go test ./internal/veraetime/ -count=1`
- [ ] **Step 5: Commit** — `feat: VeraeTime client and auth.properties`
- [ ] **Step 4: Pass** — `go test ./internal/veraetime/ ./internal/authctx/ -count=1`
- [ ] **Step 5: Commit** — `feat: VeraeTime client, auth.properties, and host-token keeper`
---
@ -194,14 +199,16 @@ git commit -m "feat: pkg/tsnats digests-only NATS timestamp client"
---
### Task 6: internal/natsbridge + stdio transport
### Task 6: Embedded NATS + stdio transport
**Files:**
- Create: `internal/natsbridge/bridge.go` wrapping `pkg/tsnats`
- Modify: config + main to enable when `VERAE_NATS_URL` set (ledger backend switch or dual)
- Add stdio serve path via go-sdk stdio transport
- Create: `internal/embednats/server.go` — start `nats-server` in-process on `VERAE_NATS_LISTEN`
- Create: `internal/embednats/handlers.go` — queue-subscribe `verae.ts.request|verify|hash.report`; call ledger/veraetime with `authctx.Resolve`; publish `verae.ts.completed` on core NATS
- Modify: main to start embed when listen addr set; stdio MCP path via go-sdk
- [ ] **Step 1–5:** TDD with fake tsnats; commit `feat: optional NATS upstream and stdio transport`
- [ ] **Step 1:** Test embed start + Stamp via `pkg/tsnats.Connect` to listen URL
- [ ] **Step 2–4:** Implement; ensure no JetStream for completed
- [ ] **Step 5: Commit** — `feat: embedded NATS with VeraeTime handlers and stdio MCP`
---

View file

@ -14,10 +14,14 @@ The live Verae MCP timestamping service is a Python process (`verae-mcp-timestam
1. Single static Go binary implementing the MCP timestamping server.
2. Full tool parity: `stamp`, `verify`, `hash_report`, `signup`, `login`, `session`, `create_user`.
3. Local/shared `FileLedger` (JSON file, mode 0600) and global VeraeTime HTTPS client.
4. Optional core-NATS bridge (same subjects as Python); **no JetStream for job-complete**.
5. Streamable HTTP + stdio transports; CORS / allowed hosts/origins compatible with ns1.
6. Shadow on ns1 beside Python; documented cutover checklist.
7. Importable **`pkg/tsnats`** NATS client for bulk app workers (MCP stays the AI path).
4. **Embedded NATS server** in-process (nats-server library); handlers call VeraeTime directly.
5. **Bearer handling (configurable):**
- `VERAE_MCP_REQUIRE_BEARER=0` (**default**): if the MCP/NATS request supplies `Authorization: Bearer`, use it (passthrough). If none is supplied, the server **obtains and keeps a current host token** (login via `auth.properties` username/password or `VERAE_GLOBAL_TOKEN`, refresh before expiry) and uses that for VeraeTime.
- `VERAE_MCP_REQUIRE_BEARER=1`: missing/invalid bearer is rejected; no host-token fallback for global VeraeTime calls.
6. Streamable HTTP + stdio transports; CORS / allowed hosts/origins compatible with ns1.
7. Shadow on ns1 beside Python; documented cutover checklist.
8. Importable **`pkg/tsnats`** client for bulk workers connecting to the embedded (or external) NATS listen address.
9. Job-complete remains **core NATS** publish/subscribe; **no JetStream for job-complete**.
## Non-goals (this project)
@ -36,21 +40,30 @@ The live Verae MCP timestamping service is a Python process (`verae-mcp-timestam
## Architecture
```
AI assistants ──MCP (stdio / Streamable HTTP)──► verae-mcp-timestamping-server-go
├── FileLedger (local|shared)
├── VeraeTime HTTPS (global)
└── optional upstream NATS (same pkg/tsnats)
AI assistants
│ MCP Streamable HTTP / stdio
│ Authorization: Bearer <user JWT>
▼
verae-mcp-timestamping-server-go (single binary)
├── MCP tools (AI) ──► FileLedger (local|shared) / VeraeTime (global)
│ bearer from MCP request context (passthrough)
├── Embedded NATS server (nats-server, in-process)
│ subscribe request/reply:
│ verae.ts.request → VeraeTime or FileLedger
│ verae.ts.verify → VeraeTime or FileLedger
│ verae.hash.report → VeraeTime or FileLedger
│ publish core notify: verae.ts.completed (no JetStream)
│ token: from NATS msg header/field OR from MCP-originated publish context
└── Listen: MCP :8001 (shadow) + NATS client port (e.g. 4223) for pkg/tsnats
Bulk apps / bus workers ──pkg/tsnats──► NATS core
request/reply: verae.ts.request | verae.ts.verify | verae.hash.report
notify (subscribe): verae.ts.completed / receipt-style subjects
(no JetStream for job-complete)
Bulk workers ──pkg/tsnats──► embedded NATS port
```
**Split:** MCP tools remain the AI surface. `pkg/tsnats` is the digests-only messaging client for large concurrent stamp batches and async completion.
**Split:** MCP tools stay for AI. Embedded NATS is the high-concurrency messaging plane; both paths call the same VeraeTime client with **per-request bearer**.
**SDK:** official `github.com/modelcontextprotocol/go-sdk/mcp` (`NewStreamableHTTPHandler`, `AddTool`, stdio).
**NATS:** `github.com/nats-io/nats.go` inside `pkg/tsnats` (and reused by `internal/natsbridge`).
**SDK:** official `github.com/modelcontextprotocol/go-sdk/mcp`.
**Embedded NATS:** `github.com/nats-io/nats-server/v2` + `nats.go`.
**Client lib:** `pkg/tsnats` connects to the embedded listen URL (or an external NATS URL for tests).
## Repository layout
@ -59,16 +72,18 @@ Remote (planned): `ssh://git@git.georgelambert.org:2223/marchon/verae-mcp-timest
```
cmd/verae-mcp-timestamping-server/main.go
pkg/tsnats/ # importable NATS timestamp client (Stamp/Verify/HashReport + Completed)
pkg/digest/ # NormalizeSHA256, AssertDigestOnly (done)
pkg/tsnats/ # importable client → embedded or external NATS
internal/config/
internal/digest/
internal/ledger/ # FileLedger
internal/veraetime/ # GlobalClient + auth.properties
internal/natsbridge/ # optional MCP upstream; wraps pkg/tsnats
internal/veraetime/ # GlobalClient; per-request Bearer override
internal/embednats/ # start embedded nats-server; register subject handlers
internal/authctx/ # extract MCP bearer → context; attach to VeraeTime + NATS
internal/tools/ # MCP tool handlers
internal/server/ # MCP HTTP/stdio wiring
docs/superpowers/specs/
docs/cutover.md # cutover checklist (deliverable)
README.md # Standard Readme
docs/cutover.md
README.md
```
Binary name: `verae-mcp-timestamping-server`
@ -92,6 +107,17 @@ Digests only: reject content/bytes/file/data/path/body/payload/blob/archive.
Normalize SHA-256 (optional `sha256:` prefix; 64 hex).
Global stamp: `restamp: false`; poll `/api/status/{jobId}` when needed.
## Auth / bearer policy
| Mode | Config | Behavior |
| --- | --- | --- |
| Host-token fallback (default) | `VERAE_MCP_REQUIRE_BEARER=0` or unset | Resolve bearer: request Bearer → else in-memory host token (mint/refresh via VeraeTime `/auth/login` using username/password from `auth.properties`, or use `VERAE_GLOBAL_TOKEN` / stored `access_token`). Never log the token. |
| Require caller bearer | `VERAE_MCP_REQUIRE_BEARER=1` | Request must include Bearer; validate with VeraeTime `/auth/validate` (or reject 401 from upstream). No host fallback for stamp/verify/hash_report global paths. |
`login` tool still writes `auth.properties` for operators. Host-token keeper loads from that file and refreshes when `expires_at` is near (skew ~60s). MCP session may cache the resolved bearer for the session lifetime when require-bearer is off.
Embedded NATS handlers use the same resolver: message/header token if present, else host token when require-bearer is off.
## pkg/tsnats (bulk app client)
Import path (planned): `git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/pkg/tsnats`
@ -119,8 +145,10 @@ Bulk guidance: prefer many in-flight `Stamp` calls (or fire request + `Subscribe
| `VERAE_MCP_PORT` | `8000` (shadow uses `8001`) |
| `VERAE_GLOBAL_URL` | `https://api.veraetime.net` |
| `VERAE_AUTH_PROPERTIES` | `$HOME/.verae/auth.properties` |
| `VERAE_GLOBAL_TOKEN` | empty (optional static host bearer) |
| `VERAE_MCP_REQUIRE_BEARER` | `0` (off = host-token fallback; `1` = require caller bearer) |
| `VERAE_LEDGER_PATH` | `$HOME/.verae/mcp-timestamp-local.json` |
| `VERAE_NATS_URL` | empty (optional) |
| `VERAE_NATS_LISTEN` | `127.0.0.1:4223` (embedded NATS client port; empty disables embed) |
| `VERAE_MCP_CORS` | unset / `1` |
| `VERAE_MCP_ALLOWED_HOSTS` | same list as Python `verae-ai.conf` |
| `VERAE_MCP_ALLOWED_ORIGINS` | same list as Python |
@ -155,9 +183,10 @@ Bulk guidance: prefer many in-flight `Stamp` calls (or fire request + `Subscribe
- `go build` produces a single runnable binary.
- Full tool list matches Python.
- Local/shared persist to ledger file; global uses VeraeTime with caller/host token.
- Optional NATS path does not use JetStream for job-complete.
- `pkg/tsnats` builds as an importable library with unit tests against a NATS test server or fake conn.
- Local/shared persist to ledger file; global uses VeraeTime with caller bearer or host-token fallback per `VERAE_MCP_REQUIRE_BEARER`.
- Embedded NATS (when enabled) handles `verae.ts.*` and does not use JetStream for job-complete.
- `pkg/tsnats` builds as an importable library with unit tests against embedded or test NATS.
- Host-token keeper refreshes before expiry when require-bearer is off.
- Shadow unit runs on ns1 without disturbing Python `:8000`.
- Cutover doc is complete enough to swap without inventing steps on the day.