VERIFISDK docsengine v2 · contract 1.5.1 · harness-sdk 1.2.0
Contract reference

Registry objects

Model, McpServer, Harness and KnowledgeBase — the four registries and the invariants each one enforces.


Four registries, four schemas, one rule: available is reachable only through a passed probe. Both invariants in reference.ts are enforced in the schema rather than left to the code that writes the records.

Source: verifi-engine-contract/src/reference.ts, plus verifi-engine/src/registry/.

Each registry proves itself differently

Giving all four the same endpoint probe was a category error the platform has already made, and it is why the models and harnesses pages sat empty looking like a data problem.

RegistryProbePass condition
McpServerthree gates over HTTPall three green and toolCount > 0
Modela real invocationinputTokens + outputTokens > 0
Harnessan image scan on a digestscanned, not stale, nothing at a blocking severity
KnowledgeBasethree gates on its MCP endpointplus indexStats.chunks > 0

The lifecycle

export const zLifecycleStatus = z.enum([
  'unregistered', 'probing', 'available', 'degraded', 'unavailable', 'retired',
]);

There is deliberately no state meaning "registered, assumed working".

register()              → status = 'probing'   (whatever the caller supplied)
probe() passes          → status = 'available'
probe() fails or throws → status = 'unavailable' + lastProbeError
setStatus(…, 'available') → THROWS
deregister()            → status = 'retired'   (blocked if a published Service depends on it)

Registry.setStatus() refuses available outright:

cannot set 'my-mcp' available directly — availability comes from a passed probe (P4)

and a status change must carry a reason: a status change must record a reason.

A probe that throws is a failed probe, never an absent one.

One nuance worth knowing: a failed probe never clears a previously discovered tool list. A probe that fell over at /healthz learned nothing about tools, so blanking the list would assert "serves nothing" on no evidence. The list keeps its own toolsProbedAt timestamp beside lastProbedAt, so a reader can see both what it served and how long ago that was true.

zMcpServer

export const zMcpServer = z.object({
  id: z.string().min(1),
  name: z.string().min(1),
  /** Slot kind this server can fill, e.g. 'web_proxy', 'recon', 'cloud', 'kb'. */
  kind: z.string().min(1),
  endpoint: z.string().url(),
  transport: z.enum(['sse', 'http', 'stdio']),
  /** Digest, not a tag — Service snapshots must be reproducible. */
  imageDigest: z.string().min(1),
  concurrencyModel: z.enum(['sidecar', 'shared', 'warm_pool']),
  shareScope: z.enum(['tenant', 'global']).default('tenant'),
  maxConcurrent: z.number().int().positive().optional(),
  status: zLifecycleStatus,
  probe: zProbeResult.optional(),
  statelessnessProbePassed: z.boolean().default(false),
  tools: z.array(zToolDescriptor).default([]),
}).strict()
  .refine((m) => m.status !== 'available' || m.probe?.passed === true, …)
  .refine((m) => m.concurrencyModel !== 'shared' || m.statelessnessProbePassed, …);
export const zToolDescriptor = z.object({
  name: z.string().min(1),
  description: z.string().optional(),
  /** Destructive tools require an approved HITL token in the CallContext. */
  destructive: z.boolean().default(false),
});

tools is populated by the probe when gate 3 succeeds — the engine's Registry.probe() writes tools and toolsProbedAt from the probe outcome.

zModel

export const zModelProvider = z.enum(['bedrock', 'anthropic', 'openai_compat', 'nim', 'vertex', 'local']);

export const zModel = z.object({
  id: z.string().min(1),
  displayName: z.string().min(1),
  provider: zModelProvider,
  /** The provider's own identifier, e.g. a Bedrock inference profile. */
  modelId: z.string().min(1),
  tier: zModelTier,
  capabilities: z.object({
    maxContextTokens: z.number().int().positive(),
    maxOutputTokens: z.number().int().positive(),
    supportsTools: z.boolean(),
  }),
  costModel: z.object({
    inputPer1kUsd: z.number().nonnegative(),
    outputPer1kUsd: z.number().nonnegative(),
  }),
  /** Always an indirection. `bedrock-sts` for the preferred STS-per-run path. */
  authRef: zCredRef,
  status: zLifecycleStatus,
  lastProbedAt: zISO.optional(),
  /** Did the probe perform a REAL invocation that returned tokens? */
  probeInvoked: z.boolean(),
}).strict()
  .refine((m) => m.status !== 'available' || m.probeInvoked, …);

costModel is per 1k tokens here. The harness SDK's own price table is per 1M tokens. Different units; do not copy numbers between them.

Both capabilities and costModel are required on the stored record and optional on the registration — the catalogue supplies them where it can, and an absent value is recorded as absent rather than defaulted.

A Model reaches available through a real invocation probe, not the three gates — see how a Model reaches available.

zHarness

The registry record for a harness. This is a different shape from the SDK's HarnessDefinition, and nothing generates one from the other.

export const zHarness = z.object({
  id: z.string().min(1),
  name: z.string().min(1),
  semver: zSemVer,
  /** The engine-contract major this harness targets; checked at handshake. */
  connectorVersion: zSemVer,
  topology: z.literal('lead_subagent'),
  requiredSlots: z.array(zMcpSlot).default([]),
  optionalSlots: z.array(zMcpSlot).default([]),
  modelRoles: z.array(zModelRole).min(1, 'a harness must declare at least one model role'),
  knowledgeBases: z.array(z.object({ domain: z.string().min(1), required: z.boolean() })).default([]),
  phases: z.array(zPhase).default([]),
  concurrency: z.object({ maxSubAgents: …, maxParallelTools: … }).optional(),
  resources: z.object({ cpu: …, memoryMb: …, maxRuntimeMinutes: … }).optional(),
  imageDigest: z.string().min(1),
  status: zLifecycleStatus,
}).strict();

Mapping from the SDK type, field by field:

HarnessDefinitionzHarness
namename (and you supply id separately)
versionsemver
connectorVersionconnectorVersion
topology?: 'lead_subagent'topologyrequired literal
slots.requiredrequiredSlots
slots.optionaloptionalSlots
modelsmodelRoles
phasesphasesorder must be a positive integer, and defineHarness now checks the same thing
knowledgeBasesknowledgeBases
concurrency, resourcessame
inputs, declarePlan, runnot represented at all
imageDigest, status supplied at registration

Nothing converts a HarnessDefinition into a zHarness; a human maps the fields at registration time. That is still an open gap — see Known gaps.

A harness serves nothing, so it has no endpoint. It is probed by checking its image.

zModelRole, zMcpSlot, zPhase

export const zModelRole = z.object({
  name: z.string().min(1),
  recommendedTier: zModelTier,
  required: z.boolean(),
  description: z.string().optional(),
}).strict();

export const zMcpSlot = z.object({
  name: z.string().min(1),
  kind: z.string().min(1),
}).strict();

export const zPhase = z.object({
  key: z.string().min(1),
  label: z.string().min(1),
  order: z.number().int().positive(),
}).strict();

A model role name is a developer-chosen string, not an enum. A harness declares as many roles as it wants — planner/attacker/scout or anything else — and the Service binds one Model to each. The same Model may fill several roles.

zKnowledgeBase

export const zKbAccessMode = z.enum(['read_only', 'propose', 'read_write']);

export const zKnowledgeBase = z.object({
  id: z.string().min(1),
  name: z.string().min(1),
  domain: z.string().min(1),
  backingStore: z.enum(['vector', 'doc', 'hybrid']),
  embeddingModelId: z.string().min(1).optional(),
  /** Pins the corpus snapshot a Service composed against. */
  contentVersion: z.string().min(1),
  indexStats: z.object({
    documents: z.number().int().nonnegative(),
    chunks: z.number().int().nonnegative(),
  }),
  status: zLifecycleStatus,
}).strict()
  .refine((k) => k.status !== 'available' || k.indexStats.chunks > 0, {
    message: 'a KB with zero chunks is not available — it answers HTTP but retrieves nothing',
  });

read_write is a real, supported mode — a human analyst curating the corpus needs it, granted at API-key creation. What is forbidden is binding it to a harness, and that gate lives on the Service; see Service, TestInput, Run.

The reason is the self-poisoning loop: a model writing unreviewed content that later models retrieve as ground truth cites one hallucinated technique forever, with no way to trace a bad finding back to it. The gate is on who writes, not on the act of writing.

Slot resolution at run time

When a Service's slots are bound, the engine checks the registry:

  • No registry entry → falls back to the configured task definition. Unregistered is not the same as unavailable, and a deployment mid-migration has real slots with no registry row.
  • Registered and not available → the run is refused:
slot 'kali' binds 'kali-mcp', which is unavailable, not available (test: core tools missing/broken: nuclei)

Otherwise available would be a label nothing acts on, which is where all of this started.

Only a shared or warm_pool slot gets its endpoint from the registry before the run starts. A sidecar gets one from the provisioner after its container comes up — copying a registry endpoint onto it would point preflight at the wrong container, possibly another tenant's.