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.
| Registry | Probe | Pass condition |
|---|---|---|
McpServer | three gates over HTTP | all three green and toolCount > 0 |
Model | a real invocation | inputTokens + outputTokens > 0 |
Harness | an image scan on a digest | scanned, not stale, nothing at a blocking severity |
KnowledgeBase | three gates on its MCP endpoint | plus 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:
HarnessDefinition | zHarness |
|---|---|
name | name (and you supply id separately) |
version | semver |
connectorVersion | connectorVersion |
topology?: 'lead_subagent' | topology — required literal |
slots.required | requiredSlots |
slots.optional | optionalSlots |
models | modelRoles |
phases | phases — order must be a positive integer, and defineHarness now checks the same thing |
knowledgeBases | knowledgeBases |
concurrency, resources | same |
inputs, declarePlan, run | not 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.