Version: 0.1
Status: Draft · Open RFC
Trust model — read this first
The forge block is a claim, not a proof. Anyone can write
"verified": true into their own manifest. A manifest is controlled by its
author; nothing in the block itself prevents forgery.
Consumers (CLIs, registries, AI clients) MUST treat the block as a cache hint and verify trust through one of two channels:
- Registry lookup — query
GET https://forgeregistry.com/api/v1/packages/:idand compare. The registry's verification records are the source of truth. - Signature verification — verify the publisher's Ed25519 signature over the canonical payload (see Signing below) against the public key served by the registry.
A consumer that renders a "verified" badge based solely on the block's contents is non-conformant with this spec.
Signing and key distribution
When a publisher runs forge publish, the CLI generates an Ed25519 keypair
(stored locally in ~/.forge/keys/) and signs the canonical payload:
forge:v1:{package_name}:{github_url}:{github_login}
The public key (SPKI PEM) and the base64 signature are stored in the
registry's verification record and served from
GET /api/v1/packages/:id as verified_publisher.publicKey and
verified_publisher.signature.
Key distribution: consumers fetch the public key from the registry at verification time. This is trust-on-first-use anchored at the registry — the registry binding key → GitHub identity is the root of trust. A transparency-log model (Sigstore) is under evaluation for v0.2 to remove the registry as a single point of trust; see the roadmap.
Key rotation: re-running forge publish with a new keypair replaces
the stored key and signature. Old signatures become invalid immediately.
Revocation
Verification can be withdrawn — e.g. a maintainer account is compromised or
a malicious update ships. Revoked entries keep their record (audit trail)
but gain a revoked block in the registry's API response:
"revoked": {
"revokedAt": "2026-06-11T09:00:00Z",
"reason": "maintainer account compromised"
}
Consumers MUST check for revoked on every verification and treat a
revoked package as untrusted regardless of any forge block contents,
signatures, or cached state. forge verify exits non-zero for revoked
packages.
Top-level structure
The forge block is added as a top-level key in your existing manifest file.
manifest root
└── forge object, optional
├── version string, required if forge block is present
├── publisher object, optional — identity & verification
├── security object, optional — scan results
├── credentials array, optional — what the user must supply to run it
├── evals object, optional — eval suites you invite people to run
└── compose object, optional — composability hints [planned]
forge.version
| Field | Type | Required |
|---|---|---|
version |
string | yes (if forge block present) |
The version of this spec the block conforms to. Must be "0.1" for this
version of the spec.
"forge": {
"version": "0.1"
}
forge.publisher
Populated automatically by Forge when a listing is claimed. You can include it manually if you prefer — but remember it is a claim: consumers verify it against the registry's records or the publisher's signature (see Trust model above), never at face value.
| Field | Type | Description |
|---|---|---|
verified |
boolean | true if Forge has verified publisher identity |
verification_method |
string | How verification was performed (see values below) |
github_login |
string | GitHub username of the verified publisher |
verified_at |
string | ISO 8601 datetime when verification occurred |
verification_method values:
| Value | Description |
|---|---|
github-owner |
GitHub login matches the repo owner |
github-collaborator-api |
Forge confirmed repo access via GitHub collaborator API |
manual |
Manually reviewed and approved by the Forge team |
"forge": {
"version": "0.1",
"publisher": {
"verified": true,
"verification_method": "github-collaborator-api",
"github_login": "sam-rivera",
"verified_at": "2026-05-27T14:30:00Z"
}
}
forge.security
Populated automatically by Forge after a security scan runs. Read-only from the publisher's perspective — Forge writes these values; they are not self-reported.
| Field | Type | Description |
|---|---|---|
scanned |
boolean | true if a scan has been run |
scan_level |
string | "base" (default) or "verified" |
last_scan |
string | ISO 8601 datetime of most recent scan |
scanner_version |
string | Version of the Forge scanner that ran |
vulnerabilities |
integer | Total known CVEs for this version (from OSV database) |
critical |
integer | Critical severity vulnerabilities |
high |
integer | High severity vulnerabilities |
moderate |
integer | Moderate severity vulnerabilities |
low |
integer | Low severity vulnerabilities |
suspicious_scripts |
boolean | true if lifecycle scripts contain flagged patterns |
status |
string | "clean" | "warnings" | "critical" | "failed" |
"forge": {
"version": "0.1",
"security": {
"scanned": true,
"scan_level": "base",
"last_scan": "2026-05-27T14:30:00Z",
"scanner_version": "0.1.0",
"vulnerabilities": 0,
"critical": 0,
"high": 0,
"moderate": 0,
"low": 0,
"suspicious_scripts": false,
"status": "clean"
}
}
forge.credentials
What a user must have in hand before your package will run. Declared by you;
displayed by Forge on the listing and scaffolded into the generated client
config as <YOUR_NAME> placeholders.
Forge stores credential names, never credential values. It does not hold
tokens, broker OAuth, or sit in the request path at runtime — this field is
metadata that helps a user get their credentials into their client. Do not
put an actual key in description: descriptions that look like they contain
one are dropped on ingest.
| Field | Type | Description |
|---|---|---|
name |
string | Environment variable name, ^[A-Z][A-Z0-9_]{1,63}$ |
label |
string | Human-readable label; derived from name when omitted |
kind |
string | "oauth" | "api-key" | "env-var"; inferred from name when omitted |
required |
boolean | Cannot run without it. Default false |
description |
string | What it is for (≤200 chars) |
obtain_url |
string | https URL where the user creates or retrieves the value |
"forge": {
"version": "0.1",
"credentials": [
{
"name": "BRAVE_API_KEY",
"label": "Brave Search API Key",
"kind": "api-key",
"required": true,
"description": "Used for every search call. The free tier is enough for personal use.",
"obtain_url": "https://brave.com/search/api/"
}
]
}
Where a declaration comes from decides how Forge presents it, in descending order of authority:
- Publisher-declared — this block, submitted by a publisher who passed the repo-ownership check. Shown as declared by the verified publisher.
- Registry-declared —
packages[].environmentVariablesin the official MCP registry, mirrored by the indexer. - Inferred — read from the
"env"object in your README's own client-config snippet, and labelled as such. Nothing inferred is ever markedrequired.
A declared list replaces the ones below it rather than adding to them. A declaration is a complete statement, so if you declare two credentials, Forge shows exactly those two — declaring the block is how you correct anything it guessed wrong.
forge.evals
The eval suites you invite people to run against your package.
This block carries no results, and that is the whole design. An earlier
draft of this spec had accuracy, latency_p50_ms and an attestation field
whose values included "forge-verified". Every one of those is a string in a
file you control: "accuracy": 0.99 costs a publisher nothing to write, and a
consumer nothing to disbelieve. A manifest cannot carry evidence, so this one
carries the commitment that makes evidence possible — which suite, at which
version, with which bytes, fetchable from where.
Results live in the registry as attributable runs, submitted by
forge eval --publish and stamped with the GitHub identity the registry
authenticated. A result is labelled reproduced only when two independent
accounts ran the same suite digest against the same version of your package and
agreed; a single run — yours included — is labelled an unverified claim and
displayed as one. See GET /api/v1/entries/:id/evals.
| Field | Type | Description |
|---|---|---|
suites |
array | Suites this package should be evaluated against (max 8) |
Each entry of suites:
| Field | Type | Description |
|---|---|---|
id |
string | Suite id, ^[a-z0-9][a-z0-9._/-]{2,63}$, e.g. forge/mcp-smoke-v1 |
version |
string | Suite version. Bump it whenever a case changes |
digest |
string | sha256:<64 hex> over the suite's canonical content |
url |
string | https URL where the suite JSON can be fetched |
"forge": {
"version": "0.1",
"evals": {
"suites": [
{
"id": "forge/mcp-smoke-v1",
"version": "1.0.0",
"digest": "sha256:4f1c…",
"url": "https://raw.githubusercontent.com/acme/server/main/evals/smoke.json"
}
]
}
}
What the digest covers. The suite's id, version, kind, and cases,
canonicalised (object keys sorted, no whitespace) and hashed with SHA-256.
description and url are excluded: they are prose and a hosting detail, and
a mirror of the same suite at a different URL must still be able to corroborate
the original. Anything that changes what is measured is inside the digest.
Compute it with forge eval (it prints the digest of any suite it runs), or
see eval-suite.schema.json for the suite format.
Why deterministic assertions only. A suite case asserts substrings, regular expressions, or error-or-not over an MCP tool response. There is no model-judged tier, because a judged result stops being reproducible the moment the judge model is deprecated or its sampling settings differ — the reporter's number would then differ from yours for reasons that have nothing to do with your package, and the registry could never tell that apart from a real disagreement.
What Forge stores, and what it derives
| Stored per run | suite reference (id, version, digest, url, case count), the exact version evaluated, passed/total, latency p50/p99, a transcript digest, the runner, when it ran, and the authenticated reporter |
| Derived, never submitted | credibility (unverified / reproduced / contested), the reported accuracy, the spread across reporters, and whether all reporters got byte-identical per-case outcomes |
| Never stored | any assertion by the reporter about how trustworthy their own run is |
A run is rejected if its case count differs from the suite's — a partial run is not a result, because the cheapest route to a perfect score would otherwise be to run only the cases you pass.
Eval results never affect the trust score. The trust score is a security
rubric (identity + scan findings) under an explainability commitment; accuracy
is a different question, and it is authored by the party being measured. Mixing
them would make the one number the registry's credibility rests on mean neither
thing. Quality ships as a parallel signal with its own badge and its own
--min-accuracy filter — which only reproduced results satisfy.
forge.compose — planned
Not yet implemented. Specified here for early adopter feedback.
| Field | Type | Description |
|---|---|---|
depends_on |
string[] | Package IDs this package calls at runtime |
can_be_called_by |
string[] | Package IDs that may invoke this package |
Complete example
{
"name": "web-researcher",
"description": "Deep web research agent",
"url": "https://example.dev/agents/web-researcher",
"version": "1.4.2",
"forge": {
"version": "0.1",
"publisher": {
"verified": true,
"verification_method": "github-collaborator-api",
"github_login": "ana",
"verified_at": "2026-05-27T14:30:00Z"
},
"security": {
"scanned": true,
"scan_level": "base",
"last_scan": "2026-05-27T14:30:00Z",
"scanner_version": "0.1.0",
"vulnerabilities": 0,
"critical": 0,
"high": 0,
"moderate": 0,
"low": 0,
"suspicious_scripts": false,
"status": "clean"
}
}
}
Usage by manifest format
MCP server (package.json)
Add the forge block as a top-level key in package.json:
{
"name": "my-mcp-server",
"version": "1.0.0",
"forge": { "version": "0.1", "publisher": { ... } }
}
A2A Agent Card (agent.json)
Add the forge block as an extension key in the Agent Card:
{
"name": "My Agent",
"url": "https://example.com/agent",
"version": "1.0",
"forge": { "version": "0.1", "publisher": { ... } }
}
SKILL.md frontmatter
Add a forge key in the YAML frontmatter:
---
name: code-reviewer
version: 1.0.0
forge:
version: "0.1"
publisher:
verified: true
github_login: sam-rivera
verified_at: "2026-05-27T14:30:00Z"
---
Changelog
v0.1 — 2026-05-27
Initial draft. Defines forge.version, forge.publisher, and forge.security.
Specifies forge.evals and forge.compose as planned fields.
2026-08-20 — forge.evals (replaces the planned draft)
Turns forge.evals from a planned results block into a declaration of the eval
suites a publisher invites people to run: suites[].{id, version, digest, url}.
The previously specified result fields (accuracy, latency_p50_ms,
total_runs, last_evaluated) and the attestation enum are removed — they
were self-reported values in a publisher-controlled file, and
attestation: "forge-verified" in particular was a claim of Forge's own
endorsement that Forge had never made. Results now live in the registry as
runs attributed to an authenticated identity, and credibility is derived from
them rather than declared. Backwards compatible in the direction that matters:
nothing was implemented against the old shape, and the field remains optional.
2026-08-20 — forge.credentials
Adds forge.credentials: the credential names a package needs, their kind
(oauth / api-key / env-var), whether each is required, and where to obtain
it. Declarative only — Forge stores no credential values, brokers no tokens, and
remains outside the runtime path. Backwards compatible: the field is optional,
and its absence means "not declared", not "needs nothing".
2026-06-11 — clarifications (no schema change)
- Added the Trust model section: the block is a claim, not a proof; consumers must verify via registry lookup or signature.
- Documented signing, key distribution, and key rotation.
- Added the Revocation section and the
revokedAPI response block.