From 8210ce0fa2315b132eddb687e8f37c9348d61bb4 Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Fri, 20 Mar 2026 10:48:48 -0400 Subject: [PATCH] [runtime] align docs with implemented runtime surface Why: bring public and agent-facing documentation into sync with the actual routes, env vars, verification path, and repo-local workflow. Contract impact: none --- AGENTS.md | 32 +++--- CLAUDE.md | 32 +++--- README.md | 252 +++++++++++++++++++++--------------------- SECURITY.md | 94 ++++++++++++---- docs/CONFIGURATION.md | 236 ++++++++++++++++++++++++--------------- docs/OPERATIONS.md | 193 +++++++++++++++++++++++--------- 6 files changed, 529 insertions(+), 310 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0fc0c9b..fc87205 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,9 +29,7 @@ CommandLayer is a layered protocol system. Understand these layers before touchi - Endpoints MUST be versioned: `/{verb}/v{protocol_version}` (e.g. `/describe/v1.0.0`) - Receipts MUST verify via `runtime-core` — no custom verification logic elsewhere - Protocol schemas are the contract; runtimes must not invent fields unless the schema allows it -- Health endpoints are required on all services: - - Router: `GET /health`, `GET /ready`, `GET /version` - - Runtimes: `GET /health`, `GET /version` +- In this repository, the implemented runtime health surface is `GET /health` plus the alias `GET /healthz`. Do not document repo-local routes that are not actually present here. --- @@ -55,7 +53,7 @@ After completing a task, review what you changed and ask: *Did I introduce any l ### >>Verification Before Done<< A task is not done until: - [ ] The targeted layer's tests pass -- [ ] Stack e2e (`./e2e.sh`) is green (or you've confirmed it's unaffected) +- [ ] Repo-local checks are green, or you have explicitly confirmed why a broader stack check is unavailable or unaffected - [ ] No hard contracts are broken - [ ] The changeset is minimal — no collateral edits @@ -86,7 +84,7 @@ Contract impact: After each step, re-read the plan. Confirm you're still on the minimal path. Bail early if scope has crept. ### >>Capture Lessons<< -If you discover something surprising about the codebase (hidden coupling, undocumented constraint, layer violation), add a note to `ARCHITECTURE.md` or the relevant `COMPATIBILITY_MATRIX.md` before finishing. +If you discover something surprising about the codebase (hidden coupling, undocumented constraint, layer violation), document it in an actually present repo doc instead of referencing files that are not checked in here. --- @@ -106,22 +104,28 @@ Prefer changes that are local to one layer. If a change ripples into two or more ## Development Workflow ### Local Setup +For this repository, use the files that are actually checked in: + ```bash +npm ci cp .env.example .env -./checkout-deps.sh # clones all dependent repos at correct refs -./inject-dockerfiles.sh # only needed if dep repos lack Dockerfiles -docker compose up -d --build -./e2e.sh # must be green before any PR +npm test +``` + +Optional local helpers that exist in this repo: + +```bash +scripts/dev.sh +node scripts/smoke.mjs ``` ### What "Green" Means -- Router is up and healthy -- Runtimes respond -- A request returns a structured receipt (or 402 if paywall enabled) -- No HTML errors or junk responses +For changes scoped to this runtime repo, "green" means the repo-local checks you touched are passing, especially `npm test` and any directly relevant smoke or workflow-covered checks. + +If a task depends on cross-repo or stack behavior, say so explicitly rather than sending users or agents to scripts that are not present in this repository. ### Dependency Versions -All repo refs are pinned in `REPO_LOCK.json`. Do not change a ref without understanding the compatibility matrix in `COMPATIBILITY_MATRIX.md`. +This repo currently pins npm dependencies through `package-lock.json`. Do not assume stack-level lockfiles or compatibility files exist here unless they are actually present. --- diff --git a/CLAUDE.md b/CLAUDE.md index 37bfd80..a5a15c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,9 +29,7 @@ CommandLayer is a layered protocol system. Understand these layers before touchi - Endpoints MUST be versioned: `/{verb}/v{protocol_version}` (e.g. `/describe/v1.0.0`) - Receipts MUST verify via `runtime-core` — no custom verification logic elsewhere - Protocol schemas are the contract; runtimes must not invent fields unless the schema allows it -- Health endpoints are required on all services: - - Router: `GET /health`, `GET /ready`, `GET /version` - - Runtimes: `GET /health`, `GET /version` +- In this repository, the implemented runtime health surface is `GET /health` plus the alias `GET /healthz`. Do not document repo-local routes that are not actually present here. --- @@ -55,7 +53,7 @@ After completing a task, review what you changed and ask: *Did I introduce any l ### >>Verification Before Done<< A task is not done until: - [ ] The targeted layer's tests pass -- [ ] Stack e2e (`./e2e.sh`) is green (or you've confirmed it's unaffected) +- [ ] Repo-local checks are green, or you have explicitly confirmed why a broader stack check is unavailable or unaffected - [ ] No hard contracts are broken - [ ] The changeset is minimal — no collateral edits @@ -86,7 +84,7 @@ Contract impact: After each step, re-read the plan. Confirm you're still on the minimal path. Bail early if scope has crept. ### >>Capture Lessons<< -If you discover something surprising about the codebase (hidden coupling, undocumented constraint, layer violation), add a note to `ARCHITECTURE.md` or the relevant `COMPATIBILITY_MATRIX.md` before finishing. +If you discover something surprising about the codebase (hidden coupling, undocumented constraint, layer violation), document it in an actually present repo doc instead of referencing files that are not checked in here. --- @@ -106,22 +104,28 @@ Prefer changes that are local to one layer. If a change ripples into two or more ## Development Workflow ### Local Setup +For this repository, use the files that are actually checked in: + ```bash +npm ci cp .env.example .env -./checkout-deps.sh # clones all dependent repos at correct refs -./inject-dockerfiles.sh # only needed if dep repos lack Dockerfiles -docker compose up -d --build -./e2e.sh # must be green before any PR +npm test +``` + +Optional local helpers that exist in this repo: + +```bash +scripts/dev.sh +node scripts/smoke.mjs ``` ### What "Green" Means -- Router is up and healthy -- Runtimes respond -- A request returns a structured receipt (or 402 if paywall enabled) -- No HTML errors or junk responses +For changes scoped to this runtime repo, "green" means the repo-local checks you touched are passing, especially `npm test` and any directly relevant smoke or workflow-covered checks. + +If a task depends on cross-repo or stack behavior, say so explicitly rather than sending users or agents to scripts that are not present in this repository. ### Dependency Versions -All repo refs are pinned in `REPO_LOCK.json`. Do not change a ref without understanding the compatibility matrix in `COMPATIBILITY_MATRIX.md`. +This repo currently pins npm dependencies through `package-lock.json`. Do not assume stack-level lockfiles or compatibility files exist here unless they are actually present. --- diff --git a/README.md b/README.md index cd8289c..77b90ee 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,183 @@ # CommandLayer Runtime -Reference Node.js runtime for CommandLayer Commons verbs. This service executes deterministic verb handlers, signs receipts with Ed25519, and verifies receipts using local keys or ENS-discovered public keys. +Reference Node.js runtime for CommandLayer Commons verbs. This service exposes deterministic verb handlers, signs receipts with Ed25519 via `@commandlayer/runtime-core`, and verifies receipts with a configured public key or an ENS lookup. -## What this service does +## What is implemented -- Exposes `POST //v1.0.0` endpoints for Commons verbs (`fetch`, `describe`, `format`, `clean`, `parse`, `summarize`, `convert`, `explain`, `analyze`, `classify`). -- Returns wrapper responses with: - - `receipt`: the signed Commons-compatible canonical receipt, - - `runtime_metadata`: optional runtime-only context such as trace and actor data, - - proof metadata (`alg`, canonical mode, SHA-256 hash, signature) kept inside `receipt.metadata.proof`. -- Exposes `POST /verify` to verify receipt hash/signature, and optionally validate schema + fetch public key from ENS. -- Includes schema validator caching, warmup queueing, SSRF protections for `fetch`, and runtime safety budgets. +The runtime currently exposes: -## API overview +- `GET /` — JSON index with service metadata and enabled verb routes. +- `GET /health` — health and signer/verifier readiness. +- `GET /healthz` — alias for `/health`. +- `POST /verify` — receipt hash/signature verification, with optional ENS lookup and optional schema validation. +- `POST //v1.0.0` for the verbs enabled by `ENABLED_VERBS`. -### Core routes +The default enabled verbs are: -- `GET /` — service index with links and enabled verbs. -- `GET /health` — process/service health and signer readiness. -- `POST //v1.0.0` — execute a single verb and return a wrapper containing a signed receipt plus optional runtime metadata. -- `POST /verify` — verify receipt integrity/signature; optional schema and ENS verification. +- `fetch` +- `describe` +- `format` +- `clean` +- `parse` +- `summarize` +- `convert` +- `explain` +- `analyze` +- `classify` -### Debug routes +## Debug routes -- `GET /debug/env` — effective runtime configuration. -- `GET /debug/enskey` — ENS TXT key discovery state. -- `GET /debug/schemafetch?verb=` — computed receipt schema URL. -- `GET /debug/validators` — validator cache and warm-queue state. -- `POST /debug/prewarm` — queue schema validator warmup. +The runtime also implements these gated debug routes: -## Quickstart +- `GET /debug/env` +- `GET /debug/enskey` +- `GET /debug/validators` +- `POST /debug/prewarm` -### 1) Install dependencies +They are available only when both of these are set: -```bash -npm install -``` +- `ENABLE_DEBUG=1` +- `DEBUG_TOKEN=` -### 2) Generate an Ed25519 keypair (for local signing) +Requests must present the token either as `Authorization: Bearer ` or `X-Debug-Token: `. When debug access is not enabled or the token is missing or wrong, these routes return `404`. -```bash -openssl genpkey -algorithm Ed25519 -out private.pem -openssl pkey -in private.pem -pubout -out public.pem +There is no `/debug/schemafetch` route in the current implementation. -export RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64="$(base64 -w0 < private.pem)" -export RECEIPT_SIGNING_PUBLIC_KEY_B64="$(openssl pkey -in public.pem -pubin -outform DER | tail -c 32 | base64 -w0)" -export RECEIPT_SIGNER_ID="runtime.commandlayer.eth" -``` +## Receipt model -> macOS note: replace `base64 -w0` with `base64 | tr -d '\n'`. +Verb routes return a JSON object with a signed `receipt` and optional unsigned `runtime_metadata`. -### 3) Start the runtime - -```bash -npm start +```json +{ + "receipt": { "...": "signed receipt" }, + "runtime_metadata": { + "trace": { "...": "optional" }, + "actor": { "...": "optional" }, + "delegation_result": { "...": "optional" } + } +} ``` -Default port is `8080` (override with `PORT`). +The signed receipt is produced by `@commandlayer/runtime-core`. The runtime sets proof fields under `receipt.metadata.proof`, including: -### 4) Verify startup +- `alg` +- `canonical` +- `signer_id` +- `kid` +- `hash_sha256` +- `signature_b64` -```bash -curl -s http://localhost:8080/health | jq . -``` +For compatibility, if `runtime-core` returns `metadata.proof.canonical`, the runtime also emits `metadata.proof.canonical_id` in responses. -You should see `"ok": true` and `"signer_ok": true`. +## Signing behavior -## Example flow +At boot, the runtime requires signer configuration unless `DEV_AUTO_KEYS=1` is set. -### Request a fetch receipt wrapper +The canonical/current environment names shown in `.env.example` are: -```bash -RECEIPT=$(curl -s -X POST "http://localhost:8080/fetch/v1.0.0" \ - -H "Content-Type: application/json" \ - -d '{ - "x402": { - "entry": "x402://fetchagent.eth/fetch/v1.0.0", - "verb": "fetch", - "version": "1.0.0" - }, - "source": "https://example.com" - }') - -printf '%s\n' "$RECEIPT" | jq . - -# canonical receipt only -printf '%s\n' "$RECEIPT" | jq '.receipt' -``` +- `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` +- `RECEIPT_SIGNING_PUBLIC_KEY_B64` +- `RECEIPT_SIGNER_ID` -### Verify the receipt locally +The implementation also accepts several legacy aliases. See [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for the exact precedence. -```bash -printf '%s' "$RECEIPT" | curl -s -X POST "http://localhost:8080/verify" \ - -H "Content-Type: application/json" \ - -d @- | jq . -``` +`RECEIPT_SIGNING_PUBLIC_KEY_B64` is the current raw-32-byte Ed25519 public key input. `RECEIPT_SIGNING_PUBLIC_KEY` is not read by the server. -### Verify with ENS public key lookup +The runtime derives `kid` from the SHA-256 fingerprint of the active 32-byte public key. It does not read `CL_KEY_ID` or `CL_CANONICAL_ID` into production signing behavior. -```bash -printf '%s' "$RECEIPT" | curl -s -X POST "http://localhost:8080/verify?ens=1" \ - -H "Content-Type: application/json" \ - -d @- | jq . -``` +## Verification behavior -## Response boundary +`POST /verify` accepts either: -Verb endpoints now return: +- a bare receipt, or +- the wrapped verb response containing `.receipt`. -```json -{ - "receipt": { "...": "signed canonical receipt" }, - "runtime_metadata": { - "trace": { "...": "optional" }, - "actor": { "...": "optional" }, - "delegation_result": { "...": "optional" } - } -} -``` +Supported query flags: -Treat `receipt` as the verifiable payload. `runtime_metadata` is runtime-only context and is not part of the signed canonical receipt. +- `ens=1` — resolve the verification public key from ENS instead of using the configured local public key. +- `strict_kid=1` — with `ens=1`, require `receipt.metadata.proof.kid` to match the ENS `cl.sig.kid` TXT value when that TXT value is present. +- `refresh=1` — refresh ENS cache before verifying. +- `schema=1` — validate the receipt against the verb receipt schema. -`POST /verify` accepts either a bare canonical receipt or the wrapped response above and will extract `.receipt` automatically when present. +When `schema=1`, schema validation uses the receipt verb to compute a schema URL under `SCHEMA_HOST`. -## Verification semantics +When `VERIFY_SCHEMA_CACHED_ONLY=1` (the default), `/verify?schema=1` returns HTTP `202` with `validator_not_warmed_yet` if the validator for that verb has not been compiled yet. `POST /debug/prewarm` can queue validator warmup, and `GET /debug/validators` shows cache state. -`POST /verify` supports query flags: +## ENS verification inputs -- `ens=1` — fetch verifier pubkey from ENS TXT records (`cl.sig.pub`, `cl.sig.canonical`, optional `cl.sig.kid`). -- `strict_kid=1` — when `ens=1` and `cl.sig.kid` exists, require canonical receipt `metadata.proof.kid` to match ENS `cl.sig.kid`. -- `refresh=1` — bypass ENS cache and refresh lookup. -- `schema=1` — validate the canonical receipt against the verb receipt schema. +When `ens=1`, the runtime resolves TXT records directly on the signer ENS name and reads: -When `VERIFY_SCHEMA_CACHED_ONLY=1` (default), schema validation is edge-safe: +- `cl.sig.pub` by default, configurable via `ENS_SIG_PUB_KEY` +- `cl.sig.kid` by default, configurable via `ENS_SIG_KID_KEY` +- `cl.sig.canonical` by default, configurable via `ENS_SIG_CANONICAL_KEY` -- if validator is warm: request is fully validated, -- if validator is cold: service returns `202` with `validator_not_warmed_yet` and queues async prewarm. +The runtime does not read `VERIFIER_ENS_NAME` or `ENS_SIGNER_TEXT_KEY`. -Use `POST /debug/prewarm` and `GET /debug/validators` for schema prewarming workflows. +`cl.sig.pub` must contain `ed25519:`. -## Configuration +## Local development -Detailed environment variable documentation lives in [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md). +### Install -### ENS TXT format (runtime.commandlayer.eth) +```bash +npm ci +``` -- `cl.sig.pub = ed25519:` -- `cl.sig.canonical = json.sorted_keys.v1` -- `cl.sig.kid = v1` (optional compatibility marker; runtime derives receipt kid from pubkey fingerprint) -- `cl.receipt.signer = runtime.commandlayer.eth` +This repository declares Node `>=20.0.0` in `package.json`. -## Security notes +### Configure -- `fetch` only allows `http(s)` URLs. -- SSRF guard blocks localhost/private IP ranges and DNS resolutions to private ranges. -- Optional host allowlist (`ALLOW_FETCH_HOSTS`) can strictly bound outbound `fetch`. -- Request-level limits are capped by server-side `SERVER_MAX_HANDLER_MS`. -- `/verify` execution is bounded by `VERIFY_MAX_MS`. +The fastest documented path is: -## Operational notes +```bash +cp .env.example .env +``` -- Validator/schema caches are in-memory (per process). -- Prewarm is best-effort and asynchronous. -- In multi-replica deployments, warm each replica independently. +Then populate the signing values from your own Ed25519 keypair, or run with `DEV_AUTO_KEYS=1` for development-only ephemeral keys. -See [`docs/OPERATIONS.md`](docs/OPERATIONS.md) for deployment and runbook guidance. +### Start the server +```bash +npm start +``` -## Deterministic signing test commands +Or use the helper script: ```bash -# mint -curl -s -X POST http://localhost:8080/describe/v1.0.0 -H "Content-Type: application/json" -d '{"x402":{"verb":"describe","version":"1.0.0","entry":"x402://describeagent.eth/describe/v1.0.0"},"input":{"subject":"CommandLayer","detail_level":"short"}}' | tee receipt.json | jq '.receipt.metadata.proof | {kid, canonical_id, hash_sha256, signature_b64}' +scripts/dev.sh +``` + +`scripts/dev.sh` generates `keys.env` with `tools/mkkeys.mjs` if needed, sources that file, enables debug routes, and starts `server.mjs` on `127.0.0.1:8099` by default. -# verify env -curl -s -X POST http://localhost:8080/verify -H "Content-Type: application/json" --data-binary @receipt.json | jq . +### Verify locally -# verify ens -curl -s -X POST "http://localhost:8080/verify?ens=1" -H "Content-Type: application/json" --data-binary @receipt.json | jq . +```bash +curl -s http://127.0.0.1:8080/health | jq . +node scripts/smoke.mjs ``` + +## CI and test surfaces + +This repository's GitHub Actions workflows currently run: + +- `npm ci` +- `npm audit --audit-level=high` +- `npm run check` +- `npm test` +- scheduled and manual `bash scripts/smoke-ens.sh` + +`npm test` runs Node unit tests plus `tests/smoke.mjs`. + +The Python SDK test file present in this repo is a placeholder and is not part of the npm-based CI path. + +## Legacy verification code + +This repository still contains `runtime/src/receipt-verification.js` and related tests under `runtime/tests/` and `sdk/typescript-sdk/tests/`. + +That code exercises an older verification model and compatibility helpers for test material in this repository. It is not the production verification path used by `server.mjs`, which imports signing and verification from `@commandlayer/runtime-core`. + +## Configuration and operations + +- Configuration reference: [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) +- Operations notes: [`docs/OPERATIONS.md`](docs/OPERATIONS.md) +- Security notes: [`SECURITY.md`](SECURITY.md) diff --git a/SECURITY.md b/SECURITY.md index 03f308f..d2d9f78 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,33 +2,87 @@ ## Reporting a Vulnerability -If you discover a security vulnerability in this project, please report it responsibly. +If you discover a security vulnerability in this project, do not open a public issue. -**Do not open a public GitHub issue for security vulnerabilities.** +Report it to: **security@commandlayer.org** -Instead, email: **security@commandlayer.org** +Include: -Please include: -- A description of the vulnerability -- Steps to reproduce the issue -- Potential impact assessment -- Suggested fix (if any) - -We will acknowledge receipt within 48 hours and provide a detailed response within 7 days. +- a description of the issue +- reproduction steps +- impact +- any suggested remediation ## Supported Versions | Version | Supported | -|---------|-----------| -| 1.0.x | Yes | +|---|---| +| `1.0.x` | Yes | + +## Current implementation notes + +This file describes controls that are enforced by the current code in `server.mjs`. + +### Signing keys + +- The runtime signs receipts at request time. +- Boot fails if valid signing configuration is missing and `DEV_AUTO_KEYS` is not enabled. +- The canonical/current variables in `.env.example` are `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64`, `RECEIPT_SIGNING_PUBLIC_KEY_B64`, and `RECEIPT_SIGNER_ID`. +- `DEV_AUTO_KEYS=1` generates an in-memory keypair for development and logs the generated material; it is not appropriate for production handling of signing keys. + +### Debug routes + +Debug routes are controlled by the implemented variables: + +- `ENABLE_DEBUG` +- `DEBUG_TOKEN` + +The server does not read `DEBUG_ROUTES_ENABLED` or `DEBUG_BEARER_TOKEN`. + +When debug access is disabled, misconfigured, or unauthorized, the debug routes return `404`. + +### CORS + +CORS is hardcoded in the current server: + +- `Access-Control-Allow-Origin: *` +- `Access-Control-Allow-Headers: Content-Type, Authorization, X-Debug-Token` +- `Access-Control-Allow-Methods: GET,POST,OPTIONS` + +There is no environment-based CORS configuration in the implementation today. + +### SSRF guard for the `fetch` verb + +The built-in SSRF guard is enabled by default with `ENABLE_SSRF_GUARD=1`. + +Current behavior blocks: + +- non-HTTP(S) schemes +- localhost names +- `169.254.169.254` +- IPv4 private/local ranges +- IPv6 literals +- hostnames whose IPv4 DNS answers resolve to blocked ranges + +`ALLOW_FETCH_HOSTS` can further restrict allowed outbound hosts. + +### Verification behavior + +Production receipt signing and verification in `server.mjs` uses `@commandlayer/runtime-core` as the cryptographic implementation. + +ENS-backed verification currently reads these TXT records directly from the signer ENS name: + +- `cl.sig.pub` by default +- `cl.sig.kid` by default +- `cl.sig.canonical` by default + +The server does not implement `VERIFIER_ENS_NAME` or `ENS_SIGNER_TEXT_KEY`. -## Security Considerations +### Controls not implemented by the current server -This runtime handles cryptographic signing and verification. Operators should: +Do not rely on these as live controls in this repository: -1. **Protect signing keys** -- never expose `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` in logs or client responses. -2. **Gate debug routes** -- set `DEBUG_ROUTES_ENABLED=0` (default) in production, or protect with `DEBUG_BEARER_TOKEN`. -3. **Restrict CORS** -- configure `CORS_ALLOW_ORIGINS` to specific origins; never use `*` in production. -4. **Enable SSRF guard** -- keep `ENABLE_SSRF_GUARD=1` (default) and use `ALLOW_FETCH_HOSTS` to restrict outbound domains. -5. **Use HTTPS** -- always deploy behind TLS termination in production. -6. **Pin dependencies** -- use `npm ci` with the lockfile for reproducible builds. +- configurable CORS env vars such as `CORS_ALLOW_ORIGINS` +- built-in rate limiting via `RATE_LIMIT_ENABLED`, `RATE_LIMIT_MAX`, or `RATE_LIMIT_WINDOW_MS` +- request-schema validation via `REQUEST_SCHEMA_VALIDATION` +- request logging via `LOG_REQUESTS` diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 4f9c265..77ca3bd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,16 +1,18 @@ # Configuration Reference -This runtime is configured via environment variables. +This file documents environment variables that are actually read by `server.mjs` today. -## Core service identity +## Core listen and service metadata -| Variable | Default | Purpose | +| Variable | Default | Notes | |---|---|---| +| `HOST` | `0.0.0.0` | HTTP bind host. | | `PORT` | `8080` | HTTP listen port. | -| `SERVICE_NAME` | `commandlayer-runtime` | Name exposed in index/health metadata. | -| `SERVICE_VERSION` | `1.0.0` | Service version exposed in responses. | -| `API_VERSION` | `1.0.0` | Version segment used in verb route shape. | -| `CANONICAL_BASE_URL` | `https://runtime.commandlayer.org` | Base URL metadata in index/health payloads. | +| `SERVICE_NAME` | `commandlayer-runtime` | Returned by `GET /` and `GET /health`. | +| `SERVICE_VERSION` | `1.0.0` | Returned by `GET /` and `GET /health`. | +| `API_VERSION` | `1.0.0` | Version segment used when mounting verb routes. | +| `CANONICAL_BASE_URL` | `https://runtime.commandlayer.org` | Returned by `GET /` and `GET /health`. | +| `RAILWAY_SERVICE_NAME` | unset | Used only in runtime trace/debug metadata; does not rename the service fields above. | ## Enabled verbs @@ -18,53 +20,114 @@ This runtime is configured via environment variables. |---|---| | `ENABLED_VERBS` | `fetch,describe,format,clean,parse,summarize,convert,explain,analyze,classify` | -Comma-separated list of enabled handlers. Disabled verbs return `404`. +The server mounts `POST //v1.0.0` only for verbs in this list. If a listed verb has no handler implementation, requests still return `404`. -## Signing + verifier identity +## Signing identity and key material -| Variable | Default | Purpose | +### Canonical/current names + +These are the current names shown in `.env.example` and accepted directly by the server: + +| Variable | Default | Behavior | |---|---|---| -| `RECEIPT_SIGNER_ID` | `runtime` (or `ENS_NAME` when set) | Receipt proof signer identifier. | -| `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` | empty | Required for signing receipts. Base64 of PEM private key. | -| `RECEIPT_SIGNING_PUBLIC_KEY_B64` | empty | **Preferred** verifier key input: base64 of raw 32-byte Ed25519 public key. | -| `RECEIPT_SIGNING_PUBLIC_KEY_PEM` | empty | Legacy verifier key input (plain PEM text). | -| `RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64` | empty | Legacy verifier key input (base64-encoded PEM); lower priority than `RECEIPT_SIGNING_PUBLIC_KEY_B64`. | -| `ENS_NAME` | empty | Optional identity alias fallback. | +| `RECEIPT_SIGNER_ID` | unset | Signer identifier written into `receipt.metadata.proof.signer_id`. Required unless a supported alias is set. | +| `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` | unset | Base64-encoded PKCS#8 Ed25519 private key PEM. Required unless a supported alias is set or `DEV_AUTO_KEYS=1`. | +| `RECEIPT_SIGNING_PUBLIC_KEY_B64` | unset | Base64-encoded raw 32-byte Ed25519 public key. Current preferred public-key input. | + +### Accepted aliases and precedence + +The server reads the first non-empty value from these lists. + +#### Signer identifier + +1. `CL_RECEIPT_SIGNER_ID` +2. `RECEIPT_SIGNER_ID` +3. `CL_RECEIPT_SIGNER` + +`CL_RECEIPT_SIGNER` is still accepted, including by `scripts/dev.sh`, but it is not the primary name in `.env.example`. + +#### Private key + +1. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM` +2. `RECEIPT_SIGNING_PRIVATE_KEY_PEM` +3. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` +4. `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` +5. `CL_RECEIPT_SIGNING_PRIVATE_KEY_B64` +6. `RECEIPT_SIGNING_PRIVATE_KEY_B64` +7. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_FILE` +8. `CL_PRIVATE_KEY_PEM` +9. `CL_PRIVATE_KEY_PEM_B64` + +Behavior: + +- `*_PEM` values are parsed as PEM text. +- `*_PEM_FILE` is read from disk. +- the remaining `*_B64` forms are base64-decoded and then parsed as PEM text. +- the private key must be PKCS#8 Ed25519 PEM. + +#### Public key + +1. `CL_RECEIPT_SIGNING_PUBLIC_KEY_B64` +2. `RECEIPT_SIGNING_PUBLIC_KEY_B64` +3. `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM` +4. `RECEIPT_SIGNING_PUBLIC_KEY_PEM` +5. `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64` +6. `RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64` +7. `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_FILE` +8. `CL_PUBLIC_KEY_B64` +9. `RECEIPT_SIGNING_PUBLIC_KEY_RAW32_B64` + +Behavior: -### Env precedence and normalization +- `*_PUBLIC_KEY_B64` and `*_RAW32_B64` are treated as raw 32-byte Ed25519 public keys. +- `*_PEM`, `*_PEM_FILE`, and `*_PEM_B64` are treated as PEM public keys. +- the server converts accepted public-key inputs to SPKI PEM internally for verification. -The runtime resolves the first non-empty value from each list: +### Startup behavior -- Private key: `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM` → `RECEIPT_SIGNING_PRIVATE_KEY_PEM` → `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` → `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` → `CL_RECEIPT_SIGNING_PRIVATE_KEY_B64` → `RECEIPT_SIGNING_PRIVATE_KEY_B64` → `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_FILE`. -- Public key: `CL_RECEIPT_SIGNING_PUBLIC_KEY_B64` → `RECEIPT_SIGNING_PUBLIC_KEY_B64` → `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM` → `RECEIPT_SIGNING_PUBLIC_KEY_PEM` → `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64` → `RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64` → `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_FILE`. -- Signer id: `CL_RECEIPT_SIGNER_ID` → `RECEIPT_SIGNER_ID`. +- If signing configuration is missing or invalid and `DEV_AUTO_KEYS` is not enabled, the process exits at boot. +- If `DEV_AUTO_KEYS=1` and valid key material is not supplied, the server generates an in-memory Ed25519 keypair and logs the generated material to stderr/stdout. +- `kid` is derived from the active public key fingerprint. It is not read from environment variables. +- `canonical_id` is fixed to `json.sorted_keys.v1` from `@commandlayer/runtime-core` and is not overridden by environment variables. -`RECEIPT_SIGNING_PUBLIC_KEY_B64` must decode to exactly 32 bytes. +### Legacy names that are present but not used for production signing behavior -## ENS-based verification +These names appear in tests or debug output, but `server.mjs` does not consume them to control live signing behavior: -| Variable | Default | Purpose | +- `CL_KEY_ID` +- `CL_CANONICAL_ID` + +## ENS verification + +| Variable | Default | Behavior | |---|---|---| -| `ETH_RPC_URL` | empty | Ethereum RPC endpoint for ENS resolver lookups. | -| `VERIFIER_ENS_NAME` | `ENS_NAME` / `RECEIPT_SIGNER_ID` fallback | ENS name queried for TXT pubkey value. | -| `ENS_SIGNER_TEXT_KEY` | `cl.receipt.signer` | ENS TXT key on verifier name that delegates to signer ENS name. | -| `ENS_SIG_PUB_TEXT_KEY` | `cl.sig.pub` | ENS TXT key on signer name containing `ed25519:` public key. | -| `ENS_SIG_KID_TEXT_KEY` | `cl.sig.kid` | ENS TXT key on signer name containing key identifier. | -| `ENS_SIG_CANONICAL_KEY` | `cl.sig.canonical` | ENS TXT key on signer name containing canonical mode (e.g. `json.sorted_keys.v1`). | +| `ETH_RPC_URL` | empty | Ethereum RPC endpoint used for live ENS lookups. Required for real `ens=1` verification unless `ENS_MOCK_TXT_JSON` is set for tests. | +| `ENS_SIG_PUB_KEY` | `cl.sig.pub` | TXT key used to fetch the Ed25519 public key on the signer ENS name. | +| `ENS_SIG_KID_KEY` | `cl.sig.kid` | TXT key used to fetch the optional kid value on the signer ENS name. | +| `ENS_SIG_CANONICAL_KEY` | `cl.sig.canonical` | TXT key used to fetch canonical mode on the signer ENS name. | +| `ENS_MOCK_TXT_JSON` | unset | Test/debug shortcut that bypasses live RPC lookups and returns mock TXT values. | -`/verify?ens=1` verifies using ENS `cl.sig.pub` key material. `/verify?ens=1&strict_kid=1` additionally enforces `cl.sig.kid` equality when present. +Important current behavior: -## Schema fetching + validation budgets +- `/verify?ens=1` resolves TXT records directly on the signer ENS name from the receipt proof, or `runtimeConfig.signerId` if the proof is missing a signer id. +- `VERIFIER_ENS_NAME` is not read. +- `ENS_SIG_PUB_TEXT_KEY` and `ENS_SIG_KID_TEXT_KEY` are not read; the implemented names are `ENS_SIG_PUB_KEY` and `ENS_SIG_KID_KEY`. +- `ENS_SIGNER_TEXT_KEY` is not read. +- `ENS_SIG_KID_KEY` is optional for normal `ens=1` verification and is only enforced when `strict_kid=1` is used and the TXT value exists. -| Variable | Default | Purpose | +## Schema fetch and verification controls + +| Variable | Default | Behavior | |---|---|---| -| `SCHEMA_HOST` | `https://www.commandlayer.org` | Schema host prefix used to compute receipt schema URLs. | -| `SCHEMA_FETCH_TIMEOUT_MS` | `15000` | Timeout per schema document fetch. | -| `SCHEMA_VALIDATE_BUDGET_MS` | `15000` | Budget for async schema compilation. | -| `VERIFY_SCHEMA_CACHED_ONLY` | `1` | If `1`, `/verify?schema=1` only uses warm validators and returns `202` on cold cache. | -| `REQUEST_SCHEMA_VALIDATION` | `0` | If `1`, validate verb request payloads against published request schemas. Returns `503` if schemas are unavailable. | +| `SCHEMA_HOST` | `https://www.commandlayer.org` | Base URL used to compute receipt schema URLs. | +| `SCHEMA_FETCH_TIMEOUT_MS` | `15000` | Timeout for schema document fetches. | +| `SCHEMA_VALIDATE_BUDGET_MS` | `15000` | Timeout budget for async AJV schema compilation. | +| `VERIFY_SCHEMA_CACHED_ONLY` | `1` | If `1`, `/verify?schema=1` uses only precompiled validators and returns `202` on a cold cache. | +| `VERIFY_MAX_MS` | `30000` | Overall timeout budget for `/verify`. | + +`REQUEST_SCHEMA_VALIDATION` is not implemented by `server.mjs`. -## Cache controls +## Schema and validator caches | Variable | Default | |---|---| @@ -72,67 +135,66 @@ The runtime resolves the first non-empty value from each list: | `JSON_CACHE_TTL_MS` | `600000` | | `MAX_VALIDATOR_CACHE_ENTRIES` | `128` | | `VALIDATOR_CACHE_TTL_MS` | `1800000` | +| `PREWARM_MAX_VERBS` | `25` | +| `PREWARM_TOTAL_BUDGET_MS` | `12000` | +| `PREWARM_PER_VERB_BUDGET_MS` | `5000` | -## Request safety limits +These settings control the in-memory schema JSON cache, compiled validator cache, and the debug prewarm worker. -| Variable | Default | Purpose | +## Request execution and fetch hardening + +| Variable | Default | Behavior | |---|---|---| -| `SERVER_MAX_HANDLER_MS` | `12000` | Hard upper bound for verb execution timeout. | -| `VERIFY_MAX_MS` | `30000` | Upper bound for `/verify` request processing. | +| `SERVER_MAX_HANDLER_MS` | `12000` | Server-side cap on verb execution time. | +| `FETCH_TIMEOUT_MS` | `8000` | Timeout for outbound `fetch` requests made by the `fetch` verb. | +| `FETCH_MAX_BYTES` | `262144` | Maximum response bytes read by the `fetch` verb. | +| `ENABLE_SSRF_GUARD` | `1` | Enables the built-in SSRF guard for the `fetch` verb. | +| `ALLOW_FETCH_HOSTS` | empty | Optional comma-separated allowlist for `fetch` hosts. | -## `fetch` hardening +Current SSRF guard behavior blocks: -| Variable | Default | Purpose | -|---|---|---| -| `FETCH_TIMEOUT_MS` | `8000` | Timeout for outbound `fetch` HTTP request. | -| `FETCH_MAX_BYTES` | `262144` | Max bytes read from outbound response body. | -| `ENABLE_SSRF_GUARD` | `1` | Enables DNS/IP/local-network SSRF checks. | -| `ALLOW_FETCH_HOSTS` | empty | Optional CSV domain allowlist (`example.com,api.example.com`). | +- non-HTTP(S) schemes +- `localhost` and `*.localhost` +- `169.254.169.254` +- IPv4 private/local ranges +- all IPv6 literals +- hostnames whose A records resolve to blocked IPv4 addresses -## CORS +## Debug route gating -| Variable | Default | Purpose | +| Variable | Default | Behavior | |---|---|---| -| `CORS_ALLOW_ORIGINS` | empty | Comma-separated list of allowed origins. Empty = deny browser-origin requests. Use `*` to allow all (not recommended in production). | -| `CORS_ALLOW_HEADERS` | `Content-Type, Authorization` | Allowed request headers. | -| `CORS_ALLOW_METHODS` | `GET,POST,OPTIONS` | Allowed HTTP methods. | +| `ENABLE_DEBUG` | `0` | Enables debug routes only when set to `1`. | +| `DEBUG_TOKEN` | empty | Required token for debug routes. If missing, debug routes still return `404`. | -## Debug routes +`DEBUG_ROUTES_ENABLED` and `DEBUG_BEARER_TOKEN` are not read by the current server. -| Variable | Default | Purpose | -|---|---|---| -| `DEBUG_ROUTES_ENABLED` | `0` | If `1`, enables `/debug/*` endpoints. Disabled by default in production. | -| `DEBUG_BEARER_TOKEN` | empty | If set, requires `Authorization: Bearer ` on all debug routes. | +## CORS -## Request logging +The current server does not expose environment-based CORS configuration. -| Variable | Default | Purpose | -|---|---|---| -| `LOG_REQUESTS` | `1` | If `1`, emits structured JSON log lines to stdout for every request. | +It always returns: -## Rate limiting +- `Access-Control-Allow-Origin: *` +- `Access-Control-Allow-Headers: Content-Type, Authorization, X-Debug-Token` +- `Access-Control-Allow-Methods: GET,POST,OPTIONS` -| Variable | Default | Purpose | -|---|---|---| -| `RATE_LIMIT_ENABLED` | `0` | If `1`, enables per-IP rate limiting. | -| `RATE_LIMIT_MAX` | `120` | Max requests per window per IP. | -| `RATE_LIMIT_WINDOW_MS` | `60000` | Sliding window duration in milliseconds. | +`CORS_ALLOW_ORIGINS`, `CORS_ALLOW_HEADERS`, and `CORS_ALLOW_METHODS` are not implemented. -## Schema prewarm behavior +## Not implemented in `server.mjs` -| Variable | Default | Purpose | -|---|---|---| -| `PREWARM_MAX_VERBS` | `25` | Max verbs accepted in one `/debug/prewarm` call. | -| `PREWARM_TOTAL_BUDGET_MS` | `12000` | Total worker runtime budget. | -| `PREWARM_PER_VERB_BUDGET_MS` | `5000` | Max warm budget per verb. | - -## Recommended production baseline - -- Set explicit signing keys and verify `signer_ok=true` and `verifier_ok=true` on `/health`. -- Keep `VERIFY_SCHEMA_CACHED_ONLY=1` for edge stability. -- Set `CORS_ALLOW_ORIGINS` to specific origins (never `*` in production). -- Set `DEBUG_ROUTES_ENABLED=0` (default) or protect with `DEBUG_BEARER_TOKEN`. -- Set `RATE_LIMIT_ENABLED=1` with appropriate limits for your traffic profile. -- Restrict egress using both network policy and `ALLOW_FETCH_HOSTS` where possible. -- Tune `FETCH_MAX_BYTES` and timeout budgets based on expected payload sizes. -- Poll `/debug/validators` after deploy and prewarm critical verbs. +These names appear in older docs or conventions but are not read by the live server: + +- `VERIFIER_ENS_NAME` +- `ENS_SIGNER_TEXT_KEY` +- `ENS_SIG_PUB_TEXT_KEY` +- `ENS_SIG_KID_TEXT_KEY` +- `CORS_ALLOW_ORIGINS` +- `CORS_ALLOW_HEADERS` +- `CORS_ALLOW_METHODS` +- `RATE_LIMIT_ENABLED` +- `RATE_LIMIT_MAX` +- `RATE_LIMIT_WINDOW_MS` +- `REQUEST_SCHEMA_VALIDATION` +- `LOG_REQUESTS` +- `RECEIPT_SIGNING_PUBLIC_KEY` diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 27c254d..cd1c2a7 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -1,77 +1,170 @@ # Operations Runbook -## Deploy checklist - -1. Set signing keys: - - `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` - - `RECEIPT_SIGNING_PUBLIC_KEY` -2. Set identity metadata: - - `RECEIPT_SIGNER_ID` - - `SERVICE_NAME`, `SERVICE_VERSION` -3. If using ENS verification: - - `ETH_RPC_URL` - - `VERIFIER_ENS_NAME` - - `ENS_SIGNER_TEXT_KEY` - - `ENS_SIG_PUB_TEXT_KEY` - - `ENS_SIG_KID_TEXT_KEY` -4. Set safety limits (`FETCH_TIMEOUT_MS`, `FETCH_MAX_BYTES`, `VERIFY_MAX_MS`). -5. Restrict outbound domains with `ALLOW_FETCH_HOSTS` where possible. - -## Post-deploy validation +This runbook describes behavior that is implemented by the current repository. + +## Minimum deployment inputs + +A normal boot requires all of the following unless `DEV_AUTO_KEYS=1` is used for development: + +- `RECEIPT_SIGNER_ID` or a supported signer-id alias +- `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` or a supported private-key alias +- `RECEIPT_SIGNING_PUBLIC_KEY_B64` or a supported public-key alias + +The current `.env.example` uses: + +- `RECEIPT_SIGNER_ID` +- `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` +- `RECEIPT_SIGNING_PUBLIC_KEY_B64` + +If these values are missing or invalid, `server.mjs` exits during boot. + +## Health and route checks + +The current server implements: + +- `GET /` +- `GET /health` +- `GET /healthz` +- `POST /verify` +- `POST //v1.0.0` for enabled verbs + +It does not implement `GET /ready` or `GET /version`. + +### Basic checks ```bash curl -s "$BASE_URL/health" | jq . -curl -s "$BASE_URL/debug/env" | jq . +curl -s "$BASE_URL/" | jq . ``` -Expected: -- `ok=true` -- `signer_ok=true` +Expected health indicators: + +- `ok: true` +- `signer_ok: true` +- `verifier_ok: true` when either a local public key is loaded or `ETH_RPC_URL` is configured - expected `enabled_verbs` -- expected timeouts/cache settings -## Schema prewarm sequence +## Debug route access + +Debug routes are disabled unless both of these are set: + +- `ENABLE_DEBUG=1` +- `DEBUG_TOKEN=` + +Use either header form: + +```bash +curl -s "$BASE_URL/debug/env" -H "X-Debug-Token: $DEBUG_TOKEN" | jq . +``` + +or: + +```bash +curl -s "$BASE_URL/debug/env" -H "Authorization: Bearer $DEBUG_TOKEN" | jq . +``` + +If debug access is not enabled correctly, the server returns `404` rather than `401` or `403`. + +## Verification and schema warmup + +`POST /verify` supports these operationally relevant query flags: + +- `ens=1` +- `strict_kid=1` +- `refresh=1` +- `schema=1` + +### Schema warmup flow + +When `VERIFY_SCHEMA_CACHED_ONLY=1`, schema verification for a cold verb returns `202` and queues warmup work. + +You can prewarm validators through the implemented debug route: ```bash curl -s -X POST "$BASE_URL/debug/prewarm" \ - -H 'content-type: application/json' \ + -H "content-type: application/json" \ + -H "X-Debug-Token: $DEBUG_TOKEN" \ -d '{"verbs":["fetch","parse","summarize","classify"]}' | jq . -curl -s "$BASE_URL/debug/validators" | jq . +curl -s "$BASE_URL/debug/validators" \ + -H "X-Debug-Token: $DEBUG_TOKEN" | jq . ``` -Repeat validator polling until required verbs appear under `cached`. +Warm state is per process. There is no shared cache across replicas. + +## ENS verification operations + +For live `ens=1` verification, the server needs: + +- `ETH_RPC_URL` +- a signer ENS name in the receipt proof, or a configured runtime signer id fallback +- TXT records for the configured keys, defaulting to: + - `cl.sig.pub` + - `cl.sig.kid` + - `cl.sig.canonical` + +Important current behavior: + +- the runtime resolves TXT records directly on the signer ENS name +- `cl.sig.kid` is optional unless the request uses `strict_kid=1` +- there is no `VERIFIER_ENS_NAME` override in the current implementation +- there is no `cl.receipt.signer` delegation step in `server.mjs` + +## Troubleshooting + +### Boot exits with signer misconfiguration + +Check that the configured private key is PKCS#8 Ed25519 PEM material and that the configured public key matches it. + +The quickest built-in check is: + +```bash +curl -s "$BASE_URL/health" | jq . +``` + +If the process is up but signing is broken, `signer_errors` in `/health` will show the validation errors accumulated at boot. + +### `/verify` returns `no public key available` + +This means neither of these paths is available: + +- a local configured public key +- `ens=1` with a successful ENS lookup + +For local verification, use `RECEIPT_SIGNING_PUBLIC_KEY_B64` or a supported alias. `RECEIPT_SIGNING_PUBLIC_KEY` is not a live server variable. + +### `/verify?schema=1` returns HTTP `202` + +This is expected when: + +- `VERIFY_SCHEMA_CACHED_ONLY=1`, and +- the validator for that verb is not warmed yet. -## Verification troubleshooting +Use `/debug/prewarm`, retry later, or disable cached-only mode. -### `no public key available` +### ENS verification fails before signature checks -- Set `RECEIPT_SIGNING_PUBLIC_KEY` (`ed25519:`) **or** use ENS verification with: - - `ETH_RPC_URL` - - `VERIFIER_ENS_NAME` - - valid `cl.sig.pub` and `cl.sig.kid` TXT values on signer ENS name. +Check: -### `validator_not_warmed_yet` with HTTP 202 +- `ETH_RPC_URL` +- signer ENS resolution +- presence and format of `cl.sig.pub` +- presence of `cl.sig.canonical` +- optional `cl.sig.kid` if you are using `strict_kid=1` -- Expected when `VERIFY_SCHEMA_CACHED_ONLY=1` and schema validator is cold. -- Trigger `/debug/prewarm` and retry `/verify?schema=1`. +### `fetch` failures -### `schema fetch failed` +The `fetch` verb is constrained by: -- Confirm schema host reachability from runtime environment. -- Check `SCHEMA_HOST`, `SCHEMA_FETCH_TIMEOUT_MS`, outbound egress rules. +- `ENABLE_SSRF_GUARD` +- `ALLOW_FETCH_HOSTS` +- `FETCH_TIMEOUT_MS` +- `FETCH_MAX_BYTES` -## Recommended observability +Blocked localhost, private-network, or disallowed-host requests fail before the outbound fetch completes. -At minimum, capture and alert on: -- HTTP 5xx rate by endpoint and verb. -- `/verify` latency and timeout count. -- `fetch` timeout/error rates. -- cold-validator 202 rate after deploy. -- cache sizes from `/debug/validators`. +## CI reality in this repo -## Hardening notes +The checked-in GitHub workflows currently run Node-based checks and a scheduled/manual ENS smoke script. -- Keep CORS policy constrained if this service is not intended for broad browser access. -- If internet fetch is not required, disable `fetch` verb via `ENABLED_VERBS`. -- Consider process isolation or egress proxy for stricter SSRF containment. +This repo also contains `sdk/python-sdk/tests/test_verification.py`, but the file is a placeholder and is not part of the npm-based CI workflow defined in `.github/workflows/ci.yml`.