VERIFISDK docsengine v2 · contract 1.5.1 · harness-sdk 1.2.0
Start here

The VERIFI engine, for developers

Build a harness that drives a security assessment, or an MCP that exposes tools to one — and prove it works before you register it.


VERIFI runs security assessments against a client's own authorized attack surface. The engine does not itself know how to test anything. It knows how to compose things that do, run them under a deadline, meter what they spend, and keep every finding they produce.

There are two things a developer outside this codebase can build:

Both have to prove they work before the engine will use them, and the two proofs are different in kind:

  • an MCP passes the three-gate probe — liveness, then readiness against its real backend, then a real MCP tools/list returning at least one tool;
  • a Model passes a real invocation that returns non-zero token counts, because a provider will list models it cannot invoke;
  • a Harness passes an image check against a sha256: digest — it serves nothing, so there is no endpoint to probe.

Everything on this site is written against source that was read, not remembered. Where a claim could not be verified against code, it says so. See Source provenance for the file list, and Known gaps for the places the SDKs are currently confusing, contradictory, or incomplete.

The object model

Six objects carry the whole system. They are defined as zod schemas in @pragyacyber/engine-contract, and that package is the normative definition — this page is a reading of it.

ObjectWhat it is
HarnessA registered, versioned container image plus the declaration of what it needs: slots, model roles, phases, inputs.
McpServerA registered tool server with an endpoint, an image digest, a concurrency model, and a probe result.
ModelA registered model with a provider, a cost model, and a flag recording whether a real invocation probe returned tokens.
KnowledgeBaseA registered corpus with a pinned contentVersion and index statistics.
ServiceThe admin composition: one harness + one model per declared role + a primary (and optional fallback) per slot + KB bindings.
RunOne execution of a Service against a target, bounded by a hard deadline.

A harness author writes against the Harness shape. An MCP author writes against the McpServer shape. Neither composes a Service — that is an admin action, and it is what supplies a run with the endpoints and model ids the harness declared it needed.

How a Service becomes a Run

A harness declares what it needs by name. A Service says what fills those names. A Run freezes what was actually used, as content digests.

AUTHORING TIME Harness declares what it needs slots.required[{ name, kind }] models[{ name, tier, req }] phases[{ key, label, order }] inputs{ … } COMPOSITION TIME Service supplies them, by name mcpBindings{ slot → primary, fallback? } modelBindings{ role → modelId } kbBindings[{ kbId, accessMode }] SUBMIT TIME RunManifest freezes what was used composition.harnessDigest composition.mcpDigests{ … } slotFills{ slot → primary, fallback? } binds by name pins digests CAUGHT AT SERVICE PUBLISH a required slot or model role left unbound blocks status: 'published' CAUGHT AT FIRST USE, NOT AT STARTUP SlotNotFilledError · ModelRoleNotBoundError thrown at the first ctx.mcp() / ctx.model()
Three objects, three different moments, and the names are the only thing that connects them. The harness never names a vendor, an endpoint or a model id — it names a slot and a role, and the service decides what fills them. That indirection is what lets one harness run against a different MCP per tenant, and it is why the run’s frozen digests are the only honest record of what a given result was actually produced by.

How a run actually happens

Engine Harness container MCP (slot) provision + probe the slot endpoints written into the manifest start container RUN_MANIFEST · RUN_TOKEN · ENGINE_GRPC_ADDR (env, not over the wire) dial Open(stream UpMessage) → stream DownMessage authorization: Bearer $RUN_TOKEN handshake at seq 0 re-sent on every re-dial runHarness: version gate → ledger → slot + model resolvers MAJOR mismatch here throws ConnectorVersionError tools/call JSON-RPC 2.0 over streamable-http, POST /mcp ToolResult { ok, text, latencyMs } a failed tool is ok:false — it never throws finding { payloadJson } emitted as produced, never batched ack(seq) advances the cursor, trims the replay buffer done(outcome), then wait ≤5s for its ack close() drains ≤2s, then exit 0 | 1 the container serves nothing and is torn down
The manifest arrives in the container’s environment, not over the channel. A harness that had to call the engine to learn what to run would have a bootstrap dependency on the very channel it is about to open. Everything after the handshake is a resumable stream: the harness holds unacked messages in a replay buffer and the ack cursor is what lets a run survive the engine being replaced underneath it.

The stream carries handshake, event, finding, heartbeat and done up, and ack, stop, approval_decision and budget_update down. The ack is a monotonic sequence cursor: a dropped stream replays from it, which is what lets a run survive the engine being replaced mid-scan. A stop can come from the deadline, an operator, or a failure — never from cost.

The manifest carries credential indirections, never credential values. Model credentials arrive in the container's environment, minted per run.

Findings are two-layer

A harness produces raw findings. A raw finding is immutable, belongs to exactly one run, and has no review status at allzRawFinding refuses reviewStatus, publishedAt, reviewedBy and clientVisible outright, and the stored zFinding is .strict() so one cannot be smuggled onto the immutable layer either.

Finding (raw, immutable, one per detection per run)
   │  keyed by dedupeKey = sha256(assetId + location + category.toLowerCase())

PublishedFinding (curated, client-visible, analyst-editable)
   │  keyed by dedupeKey across every run of an Assessment
   │  carries BOTH client-facing versions and audit run ids

the client's report

Nothing reaches a client before an analyst reviews it. There is deliberately no review status meaning "auto-approved" and no assessment status that skips in_review. As a harness author you never touch the curated layer; you emit raw observations and the engine normalises them at the boundary.

What is fixed, and what is not

PackageVersionStatus
@pragyacyber/engine-contract1.5.1Stable. Additive-only within a major, enforced by a diff gate in CI. Depend on it at ^1.0.0.
@pragyacyber/harness-sdk1.2.0Stable. createModelResolver now reads per-role provider/baseUrl/apiKey from the injected env, so a keyed (NIM/OpenAI) role works from a harness and one service can mix a Bedrock role with a keyed one. ^1.0.0 is the right range — see below.
@pragyacyber/mcp-sdk0.1.0Not the path for a production MCP. Its authoring rules are right and are taught here, but its HTTP server does not speak MCP JSON-RPC, so a server built on it cannot pass gate 3. Details. Build MCPs in Python.

What changed recently

VersionChange
harness-sdk 1.2.0createModelResolver reads a per-role VERIFI_MODEL_<ROLE>_PROVIDER / _BASEURL / _APIKEY from the env the engine injects (from its registry + Secrets Manager), so a keyed provider role (NIM / OpenAI-compatible / Anthropic / Vertex) is now usable from inside a harness and one service can mix a Bedrock lead with a keyed sub-agent. Absent env still falls back to the Bedrock defaults. Read from source; a live end-to-end NIM run is still pending. See Models.
harness-sdk 1.1.0ctx.model() now builds every request through the contract (buildModelRequest / parseModelResponse / modelEndpoint), so a model that passed its probe cannot 400 on a shape only the SDK sends — non-Anthropic Bedrock families and non-bedrock providers work now. It also resolves Fargate task-role credentials and double-encodes the SigV4 canonical URI. See Models.
contract 1.5.1buildModelRequest resolves the Bedrock model family through a cross-region inference-profile prefix. See Models.
contract 1.5.0zHarnessRegistration requires an imageDigest matching /^sha256:[0-9a-f]{64}$/. A tag is refused.
contract 1.4.3capabilities and costModel became optional on a model registration; invocationId was added.
contract 1.4.2Bedrock is a host, not a model family — each family gets its own request body, and an unknown one is refused rather than guessed at.
contract 1.4.0zRawFinding published, so ctx.emitFinding() has a real type; registration.ts added; zEventType widened to cover the six types the SDK actually emits.
harness-sdk 1.0.0ctx.target and ctx.hardDeadline exposed; redaction actually switched on; emitFinding typed RawFinding; phase order validated as a positive integer at authoring time.

Start reading