Skip to content

Releases: NVNM-Chain/nvnm-mcp-server

v1.0.0-rc18

v1.0.0-rc18 Pre-release
Pre-release

Choose a tag to compare

@andriianchestnykh-devpro andriianchestnykh-devpro released this 13 Aug 14:19
a702867

Added

  • anchor_get_registries supports filtering by name: pass name and an
    optional match (exact (default), prefix, suffix, or contains, all
    case-insensitive). Since the anchoring precompile has no by-name index,
    this scans the registry table client-side, finds every match rather
    than the first, and returns the offset/limit window of those matches
    (defaulting, as any listing does, to offset 0 and 100 rows) with
    pagination.total set to the full match count -- so no match is hidden and
    a caller can page through all of them. Registry names are caller-supplied
    and not unique, so callers should check creator/created_at to
    disambiguate. anchor_get_registry(id) stays ID-only and single-valued;
    the by-name path lives only on anchor_get_registries. (W3)

Changed

  • name can be combined with offset/limit on
    anchor_get_registries (they page the match set); combining it with
    registry_id remains rejected, and ErrInvalidFilterCombination now
    describes that narrower rule. offset/limit stay optional, with the page
    size defaulting to 100.

Deprecated

  • anchor_get_registries' registry_id parameter. It still returns that
    single registry and still rejects every listing parameter
    (name, match, non-zero offset/limit), but anchor_get_registry is
    the supported way to fetch one registry by ID.

Fixed

  • anchor_get_registries inline doc corrections: the limit field's
    schema description no longer claims chain-side pages are capped at 200 rows
    for the caller's limit -- the caller's limit is applied client-side in all
    listing paths and is never sent to the chain. The tool description now
    explicitly states that every listing mode (filtered and unfiltered) does a
    full client-side scan before windowing, not only the name-filtered path.
    The defaultRegistriesPageSize constant comment no longer implies the
    default may be applied further down in the client -- both listing branches
    resolve pagination through resolveRegistriesPage.
  • anchor_prepare_update_record_status and anchor_prepare_revoke_role
    are now reachable on keyless (anonymous-read) deployments
    , classified
    auth-exempt alongside their sibling prepare tools. (W1)
  • Auth-exemption coverage is now enforced by construction: a test walks
    the live registered tool list and asserts every tool is either
    auth-exempt or the one conditional write tool, failing and naming any
    tool that's missing a classification, instead of pinning independent
    size/count literals that could drift out of sync with each other. (W2)
  • anchor_get_registries' next_actions hint now accurately describes
    anchor_get_registry as ID-only
    , and separately points callers who
    want a name-based lookup at anchor_get_registries(name=...). (W4)
  • match without name is rejected with a curated input error instead
    of being silently ignored -- silently dropping a supplied parameter would
    leave the caller believing a filter was applied when none was.
  • The by-name scan's pagination trusts NextKey emptiness alone as its
    end-of-table signal (previously it also stopped on a short page, which
    would silently end the walk after one page if the chain's own page cap
    ever dropped below the client's requested page size). Same fix applied to
    the seed script's and integration helper's walks.
  • Each by-name scan emits a structured log line (duration, match count,
    requested offset/limit, truncated flag) so operators can watch the
    client-side scan's frequency and cost. Note that paging a match set does
    not reduce that cost: each page re-runs the full scan. The scan is a
    stopgap: if the chain gains a by-name index as expected, or if indexing is
    solved off-chain, it can be retired (#79).

Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc18-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc18-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc18-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc18-<os>-<arch>

v1.0.0-rc17

v1.0.0-rc17 Pre-release
Pre-release

Choose a tag to compare

@AaronForce1 AaronForce1 released this 05 Aug 14:22
d7fde8f

No CHANGELOG.md entry for 1.0.0-rc17; using auto-generated notes.


Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc17-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc17-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc17-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc17-<os>-<arch>

v1.0.0-rc16

v1.0.0-rc16 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Jul 19:44
v1.0.0-rc16
31e41a8

Fixed

  • evm_get_logs over-wide block ranges now return an actionable message
    ("block range too wide … narrow the range and retry") instead of the opaque
    upstream operation failed. The upstream node's range-cap rejection
    (observed as maximum [from, to] blocks distance: 10000) is recognized at
    the RPC boundary via the same curated-allowlist pattern as precompile
    reverts; the node's raw error text is never echoed. (dmome21 smoke
    2026-07-28, Finding 3)
  • evm_get_transaction not-found responses no longer double the message
    (transaction not found: transaction not found): the handler re-wrapped the
    client's own sentinel with the same prefix, which also mislabeled genuine
    upstream failures as not-found. The evm_get_logs handler had the same
    latent re-prefix and was fixed alongside. (Finding 4)
  • Empty-JSON-object metadata ({}) rejection no longer leaks its
    classifier
    (the trailing : missing required parameter): it now surfaces
    a dedicated input-class sentinel whose text is exactly the curated message —
    the value is present but invalid, not missing. (Finding 4)

Changed

  • Bumped actions/checkout v6.0.3 → v7.0.1 across all workflows,
    superseding Dependabot PR #60 (whose SHA update left the stale # v6
    comments on the pins). v7's breaking change only affects
    pull_request_target/workflow_run triggers, which no workflow here uses.
    CI-only, no image change.
  • Bumped the OpenTelemetry module family v1.43.0 → v1.44.0 (Prometheus
    exporter v0.65.0 → v0.66.0) and modelcontextprotocol/go-sdk v1.6.0 →
    v1.6.1
    in one batch, superseding Dependabot PRs #4#13. Transitive lifts:
    grpc-ecosystem/grpc-gateway/v2 v2.28.0 → v2.29.0, genproto pins, and the
    new go.opentelemetry.io/otel/metric/x v0.66.0 module (all Apache-2.0 /
    BSD-3). Routine version-update hygiene, no security advisories involved;
    vendor tree regenerated.
  • Grouped future Dependabot OpenTelemetry updates into a single PR
    (.github/dependabot.yml groups: stanza) so the
    otel module family no longer arrives as 8+ interlocking PRs that conflict on
    go.sum/vendor/.
  • Dependabot bumps merged since rc15 (recorded here for image
    provenance): pressly/goose/v3 3.27.1 → 3.27.3 (#62),
    prometheus/client_golang 1.23.2 → 1.24.0 (#61), and the
    distroless/static-debian12 base-image digest refresh (#58) — all three
    change the compiled image; docker/login-action 4.2.0 → 4.5.0 (#59) is
    CI-only.

Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc16-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc16-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc16-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc16-<os>-<arch>

v1.0.0-rc15

v1.0.0-rc15 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 22 Jul 20:56
v1.0.0-rc15
abf6982

Security

  • Scoped id-token: write (and packages/contents: write) from workflow
    level down to the specific jobs that need them
    (.github/workflows/ci.yml,
    image.yml,
    release.yml).
    All three workflows granted
    id-token: write at workflow scope, so every job in them could mint a
    Sigstore-attributable OIDC token — including jobs that never sign. Job-level
    permissions blocks replace workflow inheritance, so the token now reaches
    only the Cosign signing jobs (check, image, build); the publish job
    (release) keeps contents: write but not id-token, and any future job
    added without a block inherits the minimal contents: read default. Surfaced
    by the pre-red-team security assessment (finding SC-3). No image change.
  • Bumped google.golang.org/grpc v1.80.0 → v1.82.1 to clear a High
    Dependabot alert (GHSA gRPC-Go xDS RBAC / HTTP/2, CVSS 8.8).
    gRPC is an
    indirect dependency pulled transitively via goose and the OTLP/gRPC
    telemetry exporters. The advisory affects gRPC's xDS RBAC authorization engine
    and HTTP/2 transport server; this server never runs a gRPC server and does
    not use xDS (our only gRPC use is the OTLP exporter client), so the vulnerable
    code is not reachable — this is a hygiene bump to keep the public alert page
    clean. Pulled golang.org/x/oauth2 and genproto/googleapis/api forward in
    step (MVS; both permissively licensed). No first-party code change.
  • Made the Docker image build hermetic (Dockerfile). The
    builder stage now compiles with -mod=vendor against the committed vendor/
    tree and no longer runs go mod download, so the image build never contacts
    the module proxy. This removes the proxy-flake failure mode that hit PR #47's
    image job and makes the compiled dependency set supply-chain-deterministic
    (what is reviewed in vendor/ is exactly what is built). No change to the
    compiled binary. Spotted during the pre-red-team security assessment.
  • Masked the caller email in the public key-request log line
    (internal/mcp/keys_request_http.go).
    The
    unauthenticated POST /api/v1/keys/request handler logged the full requester
    email at INFO; it now logs only a masked form (a***@example.com) via a new
    logging.SafeEmail helper. request_id already links to the full stored
    record, so no operator capability is lost. Surfaced by the security assessment
    (finding LG-3).
  • Bounded the upstream broadcast error persisted to the write-audit trail
    (internal/mcp/tools_evm_write.go).
    The
    node-returned error string on a failed broadcast is now capped at 512
    characters before it reaches write_audit.Error or the audit log. The client
    never sees this error (SafeForClient collapses it), but an unbounded reply
    from a hostile/MITM'd node could otherwise bloat the audit column and log sink.
    Surfaced by the security assessment (finding LG-2).
  • KEY_HMAC_PEPPER now has a boot-time length floor
    (internal/config/config.go).
    A set pepper
    (active or previous) shorter than 32 characters fails boot with
    ErrPepperTooShort. Previously any non-empty value was accepted, so a weak,
    brute-forceable pepper silently weakened the key-hash MAC while giving the
    false confidence of "peppered." Unset (unpeppered) mode is unchanged. Operators
    running a pepper shorter than 32 characters must rotate to a ≥ 32-character
    high-entropy secret before upgrading. Surfaced by the pre-red-team security
    assessment (finding KS-2).
  • Hardened the untrusted RPC-node boundary against denial of service
    (internal/evm, internal/anchor).
    Node/RPC
    responses are untrusted (a plaintext http:// endpoint is permitted, so a
    hostile or MITM'd node controls the bytes). Two gaps are closed: (EV-1) every
    response body is now capped at 32 MiB via a limiting http.RoundTripper, so an
    unbounded reply can no longer exhaust process memory before decode; and (EV-2)
    the node-response decode/normalize paths now run under recover(), converting
    a malformed-response panic into an ErrNodeResponseDecode error instead of
    crashing the stdio process. This mirrors the recover() + size cap already on
    the caller-transaction decode path and satisfies supplement invariant INV-6.
    Surfaced by the pre-red-team security assessment (findings EV-1, EV-2).
  • Fuzz-hardened the untrusted decode boundary
    (internal/evm/decode_fuzz_test.go,
    internal/anchor/decode_fuzz_test.go).

    Go native fuzz targets for the caller-tx decode (11.3M execs) and the anchor
    ABI decode (3.4M execs). The caller path held every invariant (no panic
    escapes recover(); CanonicalRaw is a signer fixed-point — the
    parser-differential defense). Fuzzing also corrects an earlier
    characterization
    : the defiweb ABI decoder is not bounds-checked — an
    unguarded probe panicked it in 1.6s with a 29-byte input (slice bounds out of range [-8388608:], a length prefix decoded as a negative index). The EV-2
    anchor guard therefore defends a real, trivially-craftable hostile-node DoS,
    not a hypothetical one; that crashing input is now a regression seed.
  • Pinned the CI license-check tool: go-licenses@latest@v1.6.0
    (.github/workflows/ci.yml).
    An unpinned
    go install ...@latest resolves to whatever the module proxy serves at run
    time and executes arbitrary Go on the runner — a supply-chain sink,
    particularly in a job that carries id-token: write. It sat directly beside a
    deliberately-pinned govulncheck@v1.3.0; this closes the asymmetry. v1.6.0
    verified to run clean under the pinned Go toolchain. Surfaced by the pre-red-team
    security assessment (finding SC-2). No image change.

Fixed

  • Corrected an over-broad logging claim in the README. The Observability
    section stated sensitive data "is redacted in all log output"; in fact the
    SafeAddr/SafeURL/SafeTxData helpers redact on debug lines, while the
    write-audit broadcast lines (INFO/WARN) deliberately record the recovered
    on-chain signer, destination, and value in full (public on-chain data forming
    the audit trail). README now matches the code and cross-references
    docs/DATA_HANDLING.md. Surfaced by the security assessment (finding LG-1,
    doc leg). The in-code posture is now resolved (see Documentation below):
    logging these identifiers is retained by design; the log sink's retention is
    documented as the operator's responsibility.

Documentation

  • Corrected the audit-attribution claim in docs/SECURITY_CONSUMER_GUIDANCE.md
    (finding CG-1). "Trace which API key created or modified an offending record"
    was false: the write-audit is keyed by the recovered on-chain signer (there
    is no client_id column, and anonymous keyless writes carry no API key). The
    claim now describes signer-based attribution, with the authenticated caller's
    client_id in the audit log line.
  • Documented the write-audit log sink as outside the retention purge
    (finding LG-1, docs/DATA_HANDLING.md § 8.3). The in-process purge deletes DB
    rows only; the same signer/destination/value appear in structured audit log
    lines governed by the operator's log-pipeline retention. Logging them is
    deliberate (operator abuse-forensics; values are already public on-chain) and
    accepted — a published retention period must configure log retention to match.
  • Documented the per-signer quota as a soft ceiling under concurrency
    (finding EA-1, docs/DATA_HANDLING.md § 8.2 + SignerQuotaStore godoc). The
    non-atomic CountIncrement can over-admit past MCP_SIGNER_WRITE_RATE by
    roughly the concurrency width; an accepted trade-off for a coarse, gas-bounded
    anti-abuse throttle (an exact cap would need a per-signer lock on every
    broadcast). No behavior change.

Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>

v1.0.0-rc14

v1.0.0-rc14 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 21 Jul 21:26
v1.0.0-rc14
f48f584

Changed

  • evm_send_raw_transaction relay scope is now enforced on the authenticated
    path too, not only under keyless writes.
    The tool is a scoped anchoring
    relay
    : the server decodes each signed transaction and refuses to broadcast
    unless its destination is the anchor precompile (ANCHOR_ADDRESS) — other
    contracts, externally-owned accounts, native value transfers, and contract
    creation are rejected with no broadcast. Previously this check ran only on the
    anonymous keyless-write path; the authenticated/self-host path decoded
    best-effort and relayed any signed transaction. Now both paths share one gate
    (decodeAndScope), so a caller's signature can never be relayed through this
    server to move funds or reach an arbitrary contract. The authenticated path
    still passes the caller's original bytes through (keyless broadcasts the
    canonical re-encode). Behavior change for self-hosters: an authenticated write
    to a non-anchor destination — or a signed transaction the decoder cannot parse
    — is now rejected by default where it previously broadcast.

    New escape hatch MCP_RELAY_ALLOW_ANY (default false) restores the prior
    unscoped best-effort relay on the authenticated path only, for self-host
    operators who genuinely need to broadcast non-anchor transactions (registry
    deploys, admin ops, exotic tx types). It has no effect under keyless writes
    and is a boot error (ErrRelayAllowAnyWithKeyless) if combined with
    MCP_KEYLESS_WRITES=true — anonymous writes are always pinned to the
    precompile. Reads of any data remain unrestricted. Documented in README.md
    (Write Architecture + env table), docs/TOOL_REFERENCE.md §16, and
    .env.example.

  • Privacy Policy revised for the authless architecture, effective 2026-07-14.
    The published Policy now describes the Service as it actually runs: no
    authentication on any tool, abuse controlled by the on-chain signer address
    recovered from each broadcast, and the anchor-precompile-only relay
    restriction. The retention schedule in §8 is the one the in-code purge
    enforces (shipped in rc13).

    Validated against the codebase before publication. Corrections made as a
    result: §3(c) now discloses the method_selector column (the four-byte
    public ABI function identifier stored alongside the calldata length);
    §3(d)'s IP-logging sentence now matches the code, which logs a caller IP only
    on error, authentication-rejection, Origin-rejection, and rate-limit paths
    and never on an accepted tool call; §8's admin_audit row no longer refers to
    "account closure" (the Service has no user accounts); and §10 no longer claims
    an Origin allowlist is enforced on every request, since a request carrying
    no Origin header is not rejected on that basis — the DNS-rebinding defence the
    MCP specification actually requires.

    The file is renamed docs/NVNM_MCP_Privacy_Policy_Jun_2026.pdf
    docs/NVNM_MCP_Privacy_Policy_Jul_2026.pdf to match its new effective date;
    the four references in README.md and docs/TERMS.md are updated in the same
    commit.

Security

  • Bumped golang.org/x/crypto v0.50.0 → v0.54.0 to clear seven Dependabot
    alerts (all SSH-package vulnerabilities in that module: VerifiedPublicKeyCallback
    permission-skip, client-driven server deadlock, agent-constraint drop on key
    forwarding, @revoked auth-bypass, pathological RSA/DSA DoS, certificate-
    restriction bypass, and a CheckHostKey/Authenticate server panic). The
    vulnerable x/crypto/ssh code is not reachable from this server — no package
    in the module imports x/crypto (go mod why confirms it is a transitive-only
    requirement via goose and x/net) — so this is a hygiene bump to keep the
    alert page clean ahead of public release, not a fix for an exploitable path.
    The upgrade pulled the rest of the golang.org/x/* family up in step
    (net 0.53→0.56, sys 0.43→0.47, text 0.36→0.40, sync 0.20→0.22); all
    BSD-3-Clause. Vendor tree re-synced; go test -race ./... green.

Removed

  • The opt-in Docker Hub mirror is gone from the image workflow. image.yml
    carried a mirror that, when the repo variable DOCKERHUB_IMAGE was set,
    logged into docker.io with DOCKERHUB_USERNAME / DOCKERHUB_TOKEN and
    re-tagged each GHCR manifest onto Docker Hub. It was never enabled — the
    variable was never set, so the steps always skipped — and mirroring has in
    practice been done by hand.

    It was removed rather than left dormant. This repository is headed for public
    release, and a workflow step that references a private-registry credential
    is a reachable sink for that credential the moment one repo variable is set;
    "currently skipped" is a configuration state, not a security boundary.
    GHCR is now the sole registry this workflow publishes to, and the workflow
    holds no credential for any other.

    No supply-chain guarantee is lost. The mirror never rebuilt anything — it
    re-tagged the same content-addressable digest — so an image mirrored by hand
    (docker buildx imagetools create -t docker.io/<org>/nvnm-mcp-server:<tag> ghcr.io/nvnm-chain/nvnm-mcp-server:<tag>) is byte-identical to what the
    automated step produced and carries the same Cosign signature over that
    digest. Publishing to a second registry is now a deliberate, human act.


Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc14-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc14-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc14-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc14-<os>-<arch>

v1.0.0-rc13

v1.0.0-rc13 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Jul 22:04
v1.0.0-rc13
b4c7482

Cumulative since v1.0.0-rc10: rc11 and rc12 were cut without CHANGELOG
entries, so the changes they carried are recorded here rather than lost.

Fixed

  • Container images now report their real version. The Dockerfile built with
    -ldflags="-s -w" and no -X injection, and the image workflow passed no
    version build-arg, so the binary fell back to the hardcoded
    version.Version = "1.0.0-rc10". Every image from rc11 onward reported
    itself as 1.0.0-rc10
    — the image tag said one thing and serverInfo /
    /healthz said another. release.yml already injected the version correctly
    into the standalone binaries; the image build was the one path that did not.

    The Dockerfile now takes ARG VERSION and injects it via -X, and
    image.yml passes ${{ steps.meta.outputs.version }}. The fallback in
    internal/version is now dev, not a release-shaped number: a
    plausible-but-wrong version is worse than an obviously-unset one, because it
    fails silently. That stale constant is exactly why this went unnoticed across
    two release candidates.

Added

  • Operator-configured data retention with an enforcing purge. The four
    Postgres tables the keyless bundle writes (write_audit, signer_quota,
    signer_blacklist, admin_audit) previously had no purge, TTL, or expiry
    of any kind
    — rows accumulated indefinitely, and the retention periods our
    own documentation cited were enforced by nothing. A new in-process purge now
    enforces per-table windows: MCP_WRITE_AUDIT_RETENTION,
    MCP_WRITE_AUDIT_GRANT_ROLE_RETENTION, MCP_SIGNER_QUOTA_RETENTION,
    MCP_SIGNER_BLACKLIST_RETENTION, MCP_ADMIN_AUDIT_RETENTION, and
    MCP_RETENTION_PURGE_INTERVAL (default 1h).

    Every window defaults to unset, which means retain indefinitely — no
    existing deployment changes behavior on upgrade. Retention is deliberately
    operator-chosen rather than hardcoded: a self-hosting operator's obligations
    are theirs to determine. The point of the feature is that a window an
    operator does set is now enforced by the artifact rather than promised by a
    document. Boot fails on a negative window, on a window with a non-positive
    purge interval (it would never fire), and on a grantRole window shorter
    than the ordinary one (it would purge the administrative trail before the
    routine traffic it exists to outlive). Deletes are batched so a large first
    sweep cannot hold long row locks. See docs/DATA_HANDLING.md § 8.3 and
    docs/RUNBOOK.md § "Data retention".

  • write_audit.method_selector (migration 0005). Records the 4-byte ABI
    method selector of each broadcast. Under keyless writes every relayed
    transaction shares one destination (checkRelayScope permits only the anchor
    precompile), so to_addr could not distinguish an administrative grantRole
    call from a routine anchor write — which made the longer grantRole
    retention window our privacy policy promises unenforceable. The selector
    is derived from the loaded ABI (anchor.Client.MethodSelector), never
    hardcoded: this precompile's grantRole takes four arguments, so its selector
    is not the well-known OpenZeppelin grantRole(bytes32,address) value. A
    selector is a public function identifier and carries no caller data. Rows
    written before this migration carry an empty selector and are treated as
    ordinary writes.

  • Trusted-proxy header hardening (C3/C5). Two defense-in-depth
    controls, both gated on the existing NVNM_TRUST_PROXY_HEADERS
    (default false, unchanged): (C3) the fail-rate and anon-read
    limiters' client-IP derivation now walks a configurable number of
    trusted hops in from the right of X-Forwarded-For ++ RemoteAddr
    instead of trusting the leftmost (client-forgeable) XFF entry, so
    a forged left-prefix can no longer mint its own rate-limit bucket;
    (C5) a new requireForwardedHTTPS middleware rejects a request
    carrying an explicit non-https X-Forwarded-Proto with 403,
    catching a plaintext-downgrade path inside the trust boundary — it
    is deliberately lenient when X-Forwarded-Proto is absent (the
    ingress remains the primary, fail-closed TLS gate; see
    docs/RUNBOOK.md § "Trusted-proxy header invariants (C3/C5)" for
    the fail-open rationale). New env var NVNM_TRUSTED_PROXY_HOPS
    (int, default 1, must be >= 1; boot fails loud otherwise) sets
    the trusted chain depth (1 = single ingress, 2 = CDN +
    ingress); documented canonically in docs/DATA_HANDLING.md § 5.
    Both controls are defense-in-depth behind the deploy-side
    invariants (proxy strips inbound XFF; ingress sets XFP and
    terminates TLS), which remain operator-owned.

  • API-key hashing now supports HMAC-SHA256 under a server-held pepper
    (KEY_HMAC_PEPPER, optional; KEY_HMAC_PEPPER_PREVIOUS for one
    rotation window). Keys carry a hash_version; legacy SHA-256 keys
    (v0) continue to authenticate unchanged via versioned candidate
    lookup. Setting KEY_HMAC_PEPPER_PREVIOUS without KEY_HMAC_PEPPER
    fails boot.

  • Postgres key-store backend (KEY_STORE_BACKEND=postgres). An
    optional Postgres-backed key store for multi-replica deployments where
    all replicas must share a single authoritative api_keys table.
    File backend (KEY_STORE_BACKEND=file) remains the default and is
    unchanged. New env vars: KEY_STORE_BACKEND, KEY_STORE_DSN
    (required when postgres). Dependencies added: pgx/v5, goose/v3.

  • Advisory-locked boot-time migrations. goose migrations for the
    Postgres backend run automatically at startup under a
    pg_advisory_lock, making concurrent replica boot safe without a
    separate migration job.

  • Persisted lazy v0→v1 rehash (Postgres backend). On first
    authenticated use, a legacy v0 key (plain SHA-256) is transparently
    rehashed to v1 (HMAC-SHA256 under KEY_HMAC_PEPPER) and the updated
    digest is committed to the api_keys table.

  • Hard pepper gate (ErrPepperRequired). When
    KEY_STORE_BACKEND=postgres and AUTH_PROVIDER=apikey, boot fails
    immediately if KEY_HMAC_PEPPER is unset. Closes the risk of a
    misconfigured replica falling back to unprotected v0 hashing on a
    shared store.

  • Key expiry enforcement (Phase 4). expires_at is now enforced at
    Lookup on both file and Postgres backends. A key expires when
    now >= expires_at; a zero/NULL expires_at never expires.

  • Three-way reject taxonomy with bounded disclosure. Auth rejections
    are classified into three messages: invalid API key (no-match —
    also returned on the timing-equalised miss path so non-holders cannot
    distinguish unknown-key from known-key), key expired (matched row,
    expiry passed; appends — renew at <KEY_RENEWAL_URL> when set), and
    key revoked (matched row, enabled=false). The specific messages
    are only reachable by a caller holding the matched key's bytes.

  • KEY_DEFAULT_TTL env var (default 8760h ≈ 1 year). Applied by
    the admin REST API and key-mgmt create when no per-key ttl
    override is given. 0 means no default expiry. Does not apply to the
    static MCP_API_KEY path (always non-expiring).

  • KEY_RENEWAL_URL env var (optional). URL appended to the key expired rejection message; empty = no hint appended.

  • Admin REST API TTL support. POST /admin/keys and
    PATCH /admin/keys/{id} accept an optional "ttl" field (duration
    string; "0"/"none"/"never" = no expiry; non-positive parsed
    duration → HTTP 400). Admin responses (KeySummary) now include
    expires_at; the zero-value 0001-01-01T00:00:00Z means no expiry.

  • key-mgmt CLI TTL and renew. key-mgmt create gains --ttl <dur|0> (default from KEY_DEFAULT_TTL). New key-mgmt renew <client-id> --ttl <dur|0> subcommand extends or clears expiry from
    now; --ttl is required (the default TTL is not applied on renew).

Fixed

  • nvnm_overview no longer tells agents a keyless deployment requires an
    API key.
    The prereqs list was a package-level constant asserting that
    broadcasting needs "an API key on this server with the writer or admin
    role" — false since the authless flip, and model-visible to every connecting
    client. It is now derived from cfg.KeylessWrites, the same flag
    RequiresAuth reads to make the actual enforcement decision, so the stated
    requirement cannot drift from the enforced one. A keyless deployment now
    reports that no credential is required and that the wallet signature is the
    only identity the server sees.
  • docs/RUNBOOK.md no longer claims a hosted authless deployment "runs no
    key store at all."
    The code does not permit that: loadAPIKeys fails boot
    with ErrHTTPAuthRequired when neither MCP_API_KEY nor
    MCP_API_KEYS_FILE is set on the HTTP transport, and that guard has no
    keyless exception. Such a deployment holds an operator credential that is
    never issued to any caller; callers remain wholly anonymous, but the
    Bearer-validation path stays mounted and an invalid token draws a 401. The
    same incorrect claim is corrected in internal/config's KeylessPGDSN
    godoc.

Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc13-<os>-<arch>

v1.0.0-rc12

v1.0.0-rc12 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Jul 20:46
v1.0.0-rc12
002f238

Added

  • Trusted-proxy header hardening (C3/C5). Two defense-in-depth
    controls, both gated on the existing NVNM_TRUST_PROXY_HEADERS
    (default false, unchanged): (C3) the fail-rate and anon-read
    limiters' client-IP derivation now walks a configurable number of
    trusted hops in from the right of X-Forwarded-For ++ RemoteAddr
    instead of trusting the leftmost (client-forgeable) XFF entry, so
    a forged left-prefix can no longer mint its own rate-limit bucket;
    (C5) a new requireForwardedHTTPS middleware rejects a request
    carrying an explicit non-https X-Forwarded-Proto with 403,
    catching a plaintext-downgrade path inside the trust boundary — it
    is deliberately lenient when X-Forwarded-Proto is absent (the
    ingress remains the primary, fail-closed TLS gate; see
    docs/RUNBOOK.md § "Trusted-proxy header invariants (C3/C5)" for
    the fail-open rationale). New env var NVNM_TRUSTED_PROXY_HOPS
    (int, default 1, must be >= 1; boot fails loud otherwise) sets
    the trusted chain depth (1 = single ingress, 2 = CDN +
    ingress); documented canonically in docs/DATA_HANDLING.md § 5.
    Both controls are defense-in-depth behind the deploy-side
    invariants (proxy strips inbound XFF; ingress sets XFP and
    terminates TLS), which remain operator-owned.
  • API-key hashing now supports HMAC-SHA256 under a server-held pepper
    (KEY_HMAC_PEPPER, optional; KEY_HMAC_PEPPER_PREVIOUS for one
    rotation window). Keys carry a hash_version; legacy SHA-256 keys
    (v0) continue to authenticate unchanged via versioned candidate
    lookup. Setting KEY_HMAC_PEPPER_PREVIOUS without KEY_HMAC_PEPPER
    fails boot.
  • Postgres key-store backend (KEY_STORE_BACKEND=postgres). An
    optional Postgres-backed key store for multi-replica deployments where
    all replicas must share a single authoritative api_keys table.
    File backend (KEY_STORE_BACKEND=file) remains the default and is
    unchanged. New env vars: KEY_STORE_BACKEND, KEY_STORE_DSN
    (required when postgres). Dependencies added: pgx/v5, goose/v3.
  • Advisory-locked boot-time migrations. goose migrations for the
    Postgres backend run automatically at startup under a
    pg_advisory_lock, making concurrent replica boot safe without a
    separate migration job.
  • Persisted lazy v0→v1 rehash (Postgres backend). On first
    authenticated use, a legacy v0 key (plain SHA-256) is transparently
    rehashed to v1 (HMAC-SHA256 under KEY_HMAC_PEPPER) and the updated
    digest is committed to the api_keys table.
  • Hard pepper gate (ErrPepperRequired). When
    KEY_STORE_BACKEND=postgres and AUTH_PROVIDER=apikey, boot fails
    immediately if KEY_HMAC_PEPPER is unset. Closes the risk of a
    misconfigured replica falling back to unprotected v0 hashing on a
    shared store.
  • Key expiry enforcement (Phase 4). expires_at is now enforced at
    Lookup on both file and Postgres backends. A key expires when
    now >= expires_at; a zero/NULL expires_at never expires.
  • Three-way reject taxonomy with bounded disclosure. Auth rejections
    are classified into three messages: invalid API key (no-match —
    also returned on the timing-equalised miss path so non-holders cannot
    distinguish unknown-key from known-key), key expired (matched row,
    expiry passed; appends — renew at <KEY_RENEWAL_URL> when set), and
    key revoked (matched row, enabled=false). The specific messages
    are only reachable by a caller holding the matched key's bytes.
  • KEY_DEFAULT_TTL env var (default 8760h ≈ 1 year). Applied by
    the admin REST API and key-mgmt create when no per-key ttl
    override is given. 0 means no default expiry. Does not apply to the
    static MCP_API_KEY path (always non-expiring).
  • KEY_RENEWAL_URL env var (optional). URL appended to the key expired rejection message; empty = no hint appended.
  • Admin REST API TTL support. POST /admin/keys and
    PATCH /admin/keys/{id} accept an optional "ttl" field (duration
    string; "0"/"none"/"never" = no expiry; non-positive parsed
    duration → HTTP 400). Admin responses (KeySummary) now include
    expires_at; the zero-value 0001-01-01T00:00:00Z means no expiry.
  • key-mgmt CLI TTL and renew. key-mgmt create gains --ttl <dur|0> (default from KEY_DEFAULT_TTL). New key-mgmt renew <client-id> --ttl <dur|0> subcommand extends or clears expiry from
    now; --ttl is required (the default TTL is not applied on renew).

Fixes from the rc9 dmome21 full smoke test (testnet, real MCP client),
re-verified end-to-end against a local rc10 binary over the real MCP protocol.

Added

  • evm_call_contract accepts an optional from address so callers can
    simulate permissioned reads that check msg.sender; omitting it preserves
    the prior zero-address behavior. (smoke F6)

Changed

  • Recognized, caller-input precompile revert reasons (e.g. an oversized
    checksum) now surface to the client as an actionable message instead of the
    generic upstream operation failed. Reasons are drawn from a fixed allowlist
    at the anchor boundary and carried by a new ErrPrecompileValidation
    sentinel; raw chain text (internal type paths) still collapses. (smoke F5)

Fixed

  • nvnm_setup_verify_hash / nvnm_setup_verify_signature: a hash/signature
    mismatch is now returned as a successful result with ok: false and the
    full remediation payload (challenge, expected/recovered_address,
    next_actions) instead of an error. The SDK discards structured output when
    a handler returns an error, so the remediation the tools were designed to
    give was previously dropped over the wire. Challenge derivation is now
    documented in the tool descriptions. (smoke F2)
  • anchor_prepare_add_record: the server now rejects the empty JSON object
    {} (and whitespace-only variants) for metadata with an actionable
    message, matching the anchoring precompile, which rejects it on-chain. Prior
    guidance to "pass {}" was corrected in the tool schema, README, and
    MetaMask guide. (smoke F3a)
  • anchor_prepare_grant_role: tool description now states it requires the
    admin role only, matching enforcement; it previously inherited shared prose
    that incorrectly advertised the writer/admin/automation roles. (smoke F4)

Fixed

  • anchor_prepare_add_record input ergonomics (rc8 E2E findings). The
    anchoring precompile rejects an empty checksum_algo or metadata and caps
    checksum at 64 chars, but those constraints were undiscoverable from the
    tool schema and surfaced only as opaque gas-estimation errors. Now:
    checksum_algo and metadata are marked required in the tool schema and
    validated client-side (fail-loud, wrapping missing required parameter)
    before any RPC; and a checksum passed in its natural 0x-prefixed form is
    stripped to the bare hex digest server-side so both representations work
    (anchor_prepare_grant_role's optional record-scoping checksum is
    normalized the same way). Documented in TOOL_REFERENCE.md; the
    METAMASK_GUIDE.md and README.md examples were corrected to pass valid,
    complete inputs. No on-chain behavior change — purely input validation and
    normalization at the prepare boundary.
  • anchor_get_records honored registry_id (rc8 E2E follow-on). The
    precompile's records query is keyed by registry name, so a registry_id
    filter was silently ignored — the registry_id-based lookup modes advertised by
    the tool returned an empty set even when the registry held records (a silent
    wrong answer, not an error). GetRecords now resolves registry_id → name
    internally (an explicit registry name still wins; an unknown id fails loud
    with a resolve registry_id error). Documented the id/name interchangeability
    in TOOL_REFERENCE.md. Integration tests assert id and name return the same
    records and that a bad id errors.
  • Clarified pagination.total is not authoritative. The nvnm-testnet-1
    precompile returns pagination.total: 0 even with countTotal=true and
    records present (a chain-side limitation, not a client decode bug — the
    decode struct matches the ABI). Documented on anchor_get_records and
    anchor_get_registries that callers should page using the length of the
    returned slice plus limit/offset, not total.
  • evm_get_transaction now populates from and reports not-found explicitly
    (rc8 E2E findings).
    The transaction normalizer never mapped the recovered
    sender, so a real mined tx came back with an empty from; it is now mapped.
    Separately, a well-formed but non-existent hash yielded a zero-value object
    that read as is_pending: true with an empty hash, because the not-found
    guard only checked for a nil transaction (the RPC/decoder returns a non-nil
    empty struct). It now also treats a missing hash as not-found (a real
    transaction — pending or mined — always carries a hash) and returns
    ErrTxNotFound.
  • anchor_get_registry/anchor_get_records no longer leak the internal
    Cosmos proto type path on a not-found.
    The precompile returns a raw
    collections: not found … of type …mantrachain.anchoring.v1.Registry error
    for an unknown registry id; it is now mapped to the clean ErrRegistryNotFound
    sentinel at the anchor-client boundary.

Security

  • Tool-handler errors are now sanitized before reaching the client. Errors
    returned from a tool handler are surfaced by the MCP SDK as CallToolResult
    content, which bypassed the receiving middleware's SafeForClient (that only
    sees protocol-level method errors). As a result, raw upstream error text —
    RPC failures, gas-estimation reverts, decode errors, internal Cosmos proto
    type paths — could leak verbatim to clients. All 21 tools are now registered
    through a single addTool wrapper that routes the handler's error through
    SafeForClient: known sentinels (not-found, auth, permission, input...
Read more

v1.0.0-rc11

v1.0.0-rc11 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Jul 19:35
v1.0.0-rc11
ef7fb20

Added

  • Trusted-proxy header hardening (C3/C5). Two defense-in-depth
    controls, both gated on the existing NVNM_TRUST_PROXY_HEADERS
    (default false, unchanged): (C3) the fail-rate and anon-read
    limiters' client-IP derivation now walks a configurable number of
    trusted hops in from the right of X-Forwarded-For ++ RemoteAddr
    instead of trusting the leftmost (client-forgeable) XFF entry, so
    a forged left-prefix can no longer mint its own rate-limit bucket;
    (C5) a new requireForwardedHTTPS middleware rejects a request
    carrying an explicit non-https X-Forwarded-Proto with 403,
    catching a plaintext-downgrade path inside the trust boundary — it
    is deliberately lenient when X-Forwarded-Proto is absent (the
    ingress remains the primary, fail-closed TLS gate; see
    docs/RUNBOOK.md § "Trusted-proxy header invariants (C3/C5)" for
    the fail-open rationale). New env var NVNM_TRUSTED_PROXY_HOPS
    (int, default 1, must be >= 1; boot fails loud otherwise) sets
    the trusted chain depth (1 = single ingress, 2 = CDN +
    ingress); documented canonically in docs/DATA_HANDLING.md § 5.
    Both controls are defense-in-depth behind the deploy-side
    invariants (proxy strips inbound XFF; ingress sets XFP and
    terminates TLS), which remain operator-owned.
  • API-key hashing now supports HMAC-SHA256 under a server-held pepper
    (KEY_HMAC_PEPPER, optional; KEY_HMAC_PEPPER_PREVIOUS for one
    rotation window). Keys carry a hash_version; legacy SHA-256 keys
    (v0) continue to authenticate unchanged via versioned candidate
    lookup. Setting KEY_HMAC_PEPPER_PREVIOUS without KEY_HMAC_PEPPER
    fails boot.
  • Postgres key-store backend (KEY_STORE_BACKEND=postgres). An
    optional Postgres-backed key store for multi-replica deployments where
    all replicas must share a single authoritative api_keys table.
    File backend (KEY_STORE_BACKEND=file) remains the default and is
    unchanged. New env vars: KEY_STORE_BACKEND, KEY_STORE_DSN
    (required when postgres). Dependencies added: pgx/v5, goose/v3.
  • Advisory-locked boot-time migrations. goose migrations for the
    Postgres backend run automatically at startup under a
    pg_advisory_lock, making concurrent replica boot safe without a
    separate migration job.
  • Persisted lazy v0→v1 rehash (Postgres backend). On first
    authenticated use, a legacy v0 key (plain SHA-256) is transparently
    rehashed to v1 (HMAC-SHA256 under KEY_HMAC_PEPPER) and the updated
    digest is committed to the api_keys table.
  • Hard pepper gate (ErrPepperRequired). When
    KEY_STORE_BACKEND=postgres and AUTH_PROVIDER=apikey, boot fails
    immediately if KEY_HMAC_PEPPER is unset. Closes the risk of a
    misconfigured replica falling back to unprotected v0 hashing on a
    shared store.
  • Key expiry enforcement (Phase 4). expires_at is now enforced at
    Lookup on both file and Postgres backends. A key expires when
    now >= expires_at; a zero/NULL expires_at never expires.
  • Three-way reject taxonomy with bounded disclosure. Auth rejections
    are classified into three messages: invalid API key (no-match —
    also returned on the timing-equalised miss path so non-holders cannot
    distinguish unknown-key from known-key), key expired (matched row,
    expiry passed; appends — renew at <KEY_RENEWAL_URL> when set), and
    key revoked (matched row, enabled=false). The specific messages
    are only reachable by a caller holding the matched key's bytes.
  • KEY_DEFAULT_TTL env var (default 8760h ≈ 1 year). Applied by
    the admin REST API and key-mgmt create when no per-key ttl
    override is given. 0 means no default expiry. Does not apply to the
    static MCP_API_KEY path (always non-expiring).
  • KEY_RENEWAL_URL env var (optional). URL appended to the key expired rejection message; empty = no hint appended.
  • Admin REST API TTL support. POST /admin/keys and
    PATCH /admin/keys/{id} accept an optional "ttl" field (duration
    string; "0"/"none"/"never" = no expiry; non-positive parsed
    duration → HTTP 400). Admin responses (KeySummary) now include
    expires_at; the zero-value 0001-01-01T00:00:00Z means no expiry.
  • key-mgmt CLI TTL and renew. key-mgmt create gains --ttl <dur|0> (default from KEY_DEFAULT_TTL). New key-mgmt renew <client-id> --ttl <dur|0> subcommand extends or clears expiry from
    now; --ttl is required (the default TTL is not applied on renew).

Fixes from the rc9 dmome21 full smoke test (testnet, real MCP client),
re-verified end-to-end against a local rc10 binary over the real MCP protocol.

Added

  • evm_call_contract accepts an optional from address so callers can
    simulate permissioned reads that check msg.sender; omitting it preserves
    the prior zero-address behavior. (smoke F6)

Changed

  • Recognized, caller-input precompile revert reasons (e.g. an oversized
    checksum) now surface to the client as an actionable message instead of the
    generic upstream operation failed. Reasons are drawn from a fixed allowlist
    at the anchor boundary and carried by a new ErrPrecompileValidation
    sentinel; raw chain text (internal type paths) still collapses. (smoke F5)

Fixed

  • nvnm_setup_verify_hash / nvnm_setup_verify_signature: a hash/signature
    mismatch is now returned as a successful result with ok: false and the
    full remediation payload (challenge, expected/recovered_address,
    next_actions) instead of an error. The SDK discards structured output when
    a handler returns an error, so the remediation the tools were designed to
    give was previously dropped over the wire. Challenge derivation is now
    documented in the tool descriptions. (smoke F2)
  • anchor_prepare_add_record: the server now rejects the empty JSON object
    {} (and whitespace-only variants) for metadata with an actionable
    message, matching the anchoring precompile, which rejects it on-chain. Prior
    guidance to "pass {}" was corrected in the tool schema, README, and
    MetaMask guide. (smoke F3a)
  • anchor_prepare_grant_role: tool description now states it requires the
    admin role only, matching enforcement; it previously inherited shared prose
    that incorrectly advertised the writer/admin/automation roles. (smoke F4)

Fixed

  • anchor_prepare_add_record input ergonomics (rc8 E2E findings). The
    anchoring precompile rejects an empty checksum_algo or metadata and caps
    checksum at 64 chars, but those constraints were undiscoverable from the
    tool schema and surfaced only as opaque gas-estimation errors. Now:
    checksum_algo and metadata are marked required in the tool schema and
    validated client-side (fail-loud, wrapping missing required parameter)
    before any RPC; and a checksum passed in its natural 0x-prefixed form is
    stripped to the bare hex digest server-side so both representations work
    (anchor_prepare_grant_role's optional record-scoping checksum is
    normalized the same way). Documented in TOOL_REFERENCE.md; the
    METAMASK_GUIDE.md and README.md examples were corrected to pass valid,
    complete inputs. No on-chain behavior change — purely input validation and
    normalization at the prepare boundary.
  • anchor_get_records honored registry_id (rc8 E2E follow-on). The
    precompile's records query is keyed by registry name, so a registry_id
    filter was silently ignored — the registry_id-based lookup modes advertised by
    the tool returned an empty set even when the registry held records (a silent
    wrong answer, not an error). GetRecords now resolves registry_id → name
    internally (an explicit registry name still wins; an unknown id fails loud
    with a resolve registry_id error). Documented the id/name interchangeability
    in TOOL_REFERENCE.md. Integration tests assert id and name return the same
    records and that a bad id errors.
  • Clarified pagination.total is not authoritative. The nvnm-testnet-1
    precompile returns pagination.total: 0 even with countTotal=true and
    records present (a chain-side limitation, not a client decode bug — the
    decode struct matches the ABI). Documented on anchor_get_records and
    anchor_get_registries that callers should page using the length of the
    returned slice plus limit/offset, not total.
  • evm_get_transaction now populates from and reports not-found explicitly
    (rc8 E2E findings).
    The transaction normalizer never mapped the recovered
    sender, so a real mined tx came back with an empty from; it is now mapped.
    Separately, a well-formed but non-existent hash yielded a zero-value object
    that read as is_pending: true with an empty hash, because the not-found
    guard only checked for a nil transaction (the RPC/decoder returns a non-nil
    empty struct). It now also treats a missing hash as not-found (a real
    transaction — pending or mined — always carries a hash) and returns
    ErrTxNotFound.
  • anchor_get_registry/anchor_get_records no longer leak the internal
    Cosmos proto type path on a not-found.
    The precompile returns a raw
    collections: not found … of type …mantrachain.anchoring.v1.Registry error
    for an unknown registry id; it is now mapped to the clean ErrRegistryNotFound
    sentinel at the anchor-client boundary.

Security

  • Tool-handler errors are now sanitized before reaching the client. Errors
    returned from a tool handler are surfaced by the MCP SDK as CallToolResult
    content, which bypassed the receiving middleware's SafeForClient (that only
    sees protocol-level method errors). As a result, raw upstream error text —
    RPC failures, gas-estimation reverts, decode errors, internal Cosmos proto
    type paths — could leak verbatim to clients. All 21 tools are now registered
    through a single addTool wrapper that routes the handler's error through
    SafeForClient: known sentinels (not-found, auth, permission, input...
Read more

v1.0.0-rc0

v1.0.0-rc0 Pre-release
Pre-release

Choose a tag to compare

@AaronForce1 AaronForce1 released this 28 Jun 04:17
8a30411

Added

  • API-key hashing now supports HMAC-SHA256 under a server-held pepper
    (KEY_HMAC_PEPPER, optional; KEY_HMAC_PEPPER_PREVIOUS for one
    rotation window). Keys carry a hash_version; legacy SHA-256 keys
    (v0) continue to authenticate unchanged via versioned candidate
    lookup. Setting KEY_HMAC_PEPPER_PREVIOUS without KEY_HMAC_PEPPER
    fails boot.
  • Postgres key-store backend (KEY_STORE_BACKEND=postgres). An
    optional Postgres-backed key store for multi-replica deployments where
    all replicas must share a single authoritative api_keys table.
    File backend (KEY_STORE_BACKEND=file) remains the default and is
    unchanged. New env vars: KEY_STORE_BACKEND, KEY_STORE_DSN
    (required when postgres). Dependencies added: pgx/v5, goose/v3.
  • Advisory-locked boot-time migrations. goose migrations for the
    Postgres backend run automatically at startup under a
    pg_advisory_lock, making concurrent replica boot safe without a
    separate migration job.
  • Persisted lazy v0→v1 rehash (Postgres backend). On first
    authenticated use, a legacy v0 key (plain SHA-256) is transparently
    rehashed to v1 (HMAC-SHA256 under KEY_HMAC_PEPPER) and the updated
    digest is committed to the api_keys table.
  • Hard pepper gate (ErrPepperRequired). When
    KEY_STORE_BACKEND=postgres and AUTH_PROVIDER=apikey, boot fails
    immediately if KEY_HMAC_PEPPER is unset. Closes the risk of a
    misconfigured replica falling back to unprotected v0 hashing on a
    shared store.
  • Key expiry enforcement (Phase 4). expires_at is now enforced at
    Lookup on both file and Postgres backends. A key expires when
    now >= expires_at; a zero/NULL expires_at never expires.
  • Three-way reject taxonomy with bounded disclosure. Auth rejections
    are classified into three messages: invalid API key (no-match —
    also returned on the timing-equalised miss path so non-holders cannot
    distinguish unknown-key from known-key), key expired (matched row,
    expiry passed; appends — renew at <KEY_RENEWAL_URL> when set), and
    key revoked (matched row, enabled=false). The specific messages
    are only reachable by a caller holding the matched key's bytes.
  • KEY_DEFAULT_TTL env var (default 8760h ≈ 1 year). Applied by
    the admin REST API and key-mgmt create when no per-key ttl
    override is given. 0 means no default expiry. Does not apply to the
    static MCP_API_KEY path (always non-expiring).
  • KEY_RENEWAL_URL env var (optional). URL appended to the key expired rejection message; empty = no hint appended.
  • Admin REST API TTL support. POST /admin/keys and
    PATCH /admin/keys/{id} accept an optional "ttl" field (duration
    string; "0"/"none"/"never" = no expiry; non-positive parsed
    duration → HTTP 400). Admin responses (KeySummary) now include
    expires_at; the zero-value 0001-01-01T00:00:00Z means no expiry.
  • key-mgmt CLI TTL and renew. key-mgmt create gains --ttl <dur|0> (default from KEY_DEFAULT_TTL). New key-mgmt renew <client-id> --ttl <dur|0> subcommand extends or clears expiry from
    now; --ttl is required (the default TTL is not applied on renew).

Fixes from the rc9 dmome21 full smoke test (testnet, real MCP client),
re-verified end-to-end against a local rc10 binary over the real MCP protocol.

Added

  • evm_call_contract accepts an optional from address so callers can
    simulate permissioned reads that check msg.sender; omitting it preserves
    the prior zero-address behavior. (smoke F6)

Changed

  • Recognized, caller-input precompile revert reasons (e.g. an oversized
    checksum) now surface to the client as an actionable message instead of the
    generic upstream operation failed. Reasons are drawn from a fixed allowlist
    at the anchor boundary and carried by a new ErrPrecompileValidation
    sentinel; raw chain text (internal type paths) still collapses. (smoke F5)

Fixed

  • nvnm_setup_verify_hash / nvnm_setup_verify_signature: a hash/signature
    mismatch is now returned as a successful result with ok: false and the
    full remediation payload (challenge, expected/recovered_address,
    next_actions) instead of an error. The SDK discards structured output when
    a handler returns an error, so the remediation the tools were designed to
    give was previously dropped over the wire. Challenge derivation is now
    documented in the tool descriptions. (smoke F2)
  • anchor_prepare_add_record: the server now rejects the empty JSON object
    {} (and whitespace-only variants) for metadata with an actionable
    message, matching the anchoring precompile, which rejects it on-chain. Prior
    guidance to "pass {}" was corrected in the tool schema, README, and
    MetaMask guide. (smoke F3a)
  • anchor_prepare_grant_role: tool description now states it requires the
    admin role only, matching enforcement; it previously inherited shared prose
    that incorrectly advertised the writer/admin/automation roles. (smoke F4)

Fixed

  • anchor_prepare_add_record input ergonomics (rc8 E2E findings). The
    anchoring precompile rejects an empty checksum_algo or metadata and caps
    checksum at 64 chars, but those constraints were undiscoverable from the
    tool schema and surfaced only as opaque gas-estimation errors. Now:
    checksum_algo and metadata are marked required in the tool schema and
    validated client-side (fail-loud, wrapping missing required parameter)
    before any RPC; and a checksum passed in its natural 0x-prefixed form is
    stripped to the bare hex digest server-side so both representations work
    (anchor_prepare_grant_role's optional record-scoping checksum is
    normalized the same way). Documented in TOOL_REFERENCE.md; the
    METAMASK_GUIDE.md and README.md examples were corrected to pass valid,
    complete inputs. No on-chain behavior change — purely input validation and
    normalization at the prepare boundary.
  • anchor_get_records honored registry_id (rc8 E2E follow-on). The
    precompile's records query is keyed by registry name, so a registry_id
    filter was silently ignored — the registry_id-based lookup modes advertised by
    the tool returned an empty set even when the registry held records (a silent
    wrong answer, not an error). GetRecords now resolves registry_id → name
    internally (an explicit registry name still wins; an unknown id fails loud
    with a resolve registry_id error). Documented the id/name interchangeability
    in TOOL_REFERENCE.md. Integration tests assert id and name return the same
    records and that a bad id errors.
  • Clarified pagination.total is not authoritative. The nvnm-testnet-1
    precompile returns pagination.total: 0 even with countTotal=true and
    records present (a chain-side limitation, not a client decode bug — the
    decode struct matches the ABI). Documented on anchor_get_records and
    anchor_get_registries that callers should page using the length of the
    returned slice plus limit/offset, not total.
  • evm_get_transaction now populates from and reports not-found explicitly
    (rc8 E2E findings).
    The transaction normalizer never mapped the recovered
    sender, so a real mined tx came back with an empty from; it is now mapped.
    Separately, a well-formed but non-existent hash yielded a zero-value object
    that read as is_pending: true with an empty hash, because the not-found
    guard only checked for a nil transaction (the RPC/decoder returns a non-nil
    empty struct). It now also treats a missing hash as not-found (a real
    transaction — pending or mined — always carries a hash) and returns
    ErrTxNotFound.
  • anchor_get_registry/anchor_get_records no longer leak the internal
    Cosmos proto type path on a not-found.
    The precompile returns a raw
    collections: not found … of type …mantrachain.anchoring.v1.Registry error
    for an unknown registry id; it is now mapped to the clean ErrRegistryNotFound
    sentinel at the anchor-client boundary.

Security

  • Tool-handler errors are now sanitized before reaching the client. Errors
    returned from a tool handler are surfaced by the MCP SDK as CallToolResult
    content, which bypassed the receiving middleware's SafeForClient (that only
    sees protocol-level method errors). As a result, raw upstream error text —
    RPC failures, gas-estimation reverts, decode errors, internal Cosmos proto
    type paths — could leak verbatim to clients. All 21 tools are now registered
    through a single addTool wrapper that routes the handler's error through
    SafeForClient: known sentinels (not-found, auth, permission, input) pass
    through unchanged; everything else collapses to a generic upstream-failure
    message. Internal-implementation disclosure, not credential exposure (keys and
    tokens are never placed in error text), but closed as defense-in-depth.
  • Bumped indirect btcd to v0.24.2 (clears Dependabot HIGH GHSA-27vh-h6mc-q6g8). Not a reachable risk: govulncheck reports 0 affected and nothing imports the btcd root module (go-eth uses the separate btcec/v2); the bump clears the graph-level alert. CONTRIBUTING.md §8 gained a Dependabot-vs-govulncheck triage note for vendored deps.

Changed

  • Stateless multi-replica operation (Option 0). The MCP Streamable-HTTP
    handler now runs with StreamableHTTPOptions{Stateless: true}. The server no
    longer keeps a per-pod session map, so any replica can serve any request and
    no load-balancer session affinity is required — plain round-robin scales
    throughput.
  • Write approval is now the client/agent's responsibility, not the server's.
    The server-side MCP elicitation prompt before evm_send_raw_transaction was
    the only server→client request and the sole reason sessions had to be sticky;
    it has been removed. Writes gate on RBAC role (writer/admin/automation)
    plus ENABLE_WRITE_TOOLS only. The `initialize...
Read more