Service, TestInput, Run
The composition an admin builds, the inputs for one execution, the manifest your container receives, and the run's own record.
Service the admin composition: harness + models + slots + KBs
└─ TestInput the inputs for ONE execution
└─ RunManifest immutable, fully resolved, injected into your container
└─ Run the execution's own recordSources: verifi-engine-contract/src/service.ts, run.ts, domain.ts.
Declare, bind, freeze
zService
export const zService = z.object({
id: z.string().min(1),
name: z.string().min(1),
harnessId: z.string().min(1),
harnessSemver: zSemVer,
/** role name → Model id. The same Model may fill several roles. */
modelBindings: z.record(z.string().min(1), z.string().min(1)),
/** slot name → { primary, fallback? } */
mcpBindings: z.record(z.string().min(1), zSlotBinding),
kbBindings: z.array(zKbBinding).default([]),
/** Copied from the Harness at compose time so publish validation is self-contained. */
requiredSlots: z.array(z.string().min(1)).default([]),
requiredModelRoles: z.array(z.string().min(1)).default([]),
defaultInputs: z.record(z.string(), z.unknown()).default({}),
status: z.enum(['draft', 'published', 'deprecated']),
semverCompat: zSemVer,
version: z.number().int().positive(),
}).strict();Validation gates publish, not draft. A half-composed draft is a normal state; a published Service with an unbound required slot is not. On publish:
- every
requiredSlotsentry must have a primary binding, elserequired slot 'kali' has no primary binding — cannot publish; - every
requiredModelRolesentry must be bound, elserequired model role 'planner' is unbound — cannot publish.
And at any status, a KB binding with accessMode: 'read_write' is refused:
a Service may not bind read_write — a Harness gets read_only or propose onlyzCompositionSnapshot
The frozen copy captured into every manifest.
export const zCompositionSnapshot = z.object({
serviceId: z.string().min(1),
serviceVersion: z.number().int().positive(),
harnessDigest: z.string().regex(/^sha256:[0-9a-f]+$/, 'must be a digest, not a tag'),
mcpDigests: z.record(z.string(), z.string().regex(/^sha256:[0-9a-f]+$/, 'must be a digest, not a tag')),
modelIds: z.record(z.string(), z.string().min(1)),
kbContentVersions: z.record(z.string(), z.string().min(1)),
capturedAt: zISO,
}).strict();Digests, not tags. "What exactly ran" has to be answerable months later for audit, and a tag is a moving target.
modelIds is the map ctx.model(role) resolves against. If a role is absent,
ModelRoleNotBoundError.
zTestInput
export const zTestInput = z.object({
id: z.string().min(1),
serviceId: z.string().min(1),
binding: zRunBinding,
target: zTargetScope,
parameters: z.record(z.string(), z.unknown()).default({}),
constraints: z.object({ budget: zBudget, maxConcurrency: z.number().int().positive() }).strict(),
policy: zRunPolicy,
metadata: z.object({
requestedBy: z.string().min(1),
reason: z.string().min(1),
ticketRef: z.string().optional(),
}).strict(),
}).strict();metadata.reason is required and non-empty. Every run records why it was asked for.
zTargetScope
export const zTargetScope = z.object({
scope: z.array(z.string().url()).min(1, 'a run needs at least one in-scope target'),
excludes: z.array(z.string().url()).default([]),
authMode: z.enum(['none', 'credentialed', 'session_token']),
/** Target credentials, by indirection only. Resolved into the manifest in flight. */
credentialRef: zCredRef.optional(),
}).strict();Scope entries are full URLs, parsed as URLs.
zRunPolicy
export const zRunPolicy = z.object({
/** Default ON. Redaction is a property of the pipeline, applied once at the boundary. */
redaction: z.enum(['off', 'standard', 'strict']).default('standard'),
hitl: z.enum(['auto', 'approve_destructive', 'approve_all']),
/** Rules of engagement. `deniedActions` wins over `allowedActions`. */
allowedActions: z.array(z.string()).default([]),
deniedActions: z.array(z.string()).default([]),
}).strict();This arrives on your context as ctx.policy. Nothing in the SDK enforces it —
allowedActions / deniedActions are free strings the SDK never reads. If your harness
takes actions that a rule of engagement could forbid, you must honour it yourself.
zRunManifest
The immutable, fully-resolved packet injected into your container.
export const zRunManifest = z.object({
runId: z.string().min(1),
correlationId: z.string().min(1),
binding: zRunBinding,
target: zTargetScope,
parameters: z.record(z.string(), z.unknown()).default({}),
composition: zCompositionSnapshot,
budget: zBudget,
policy: zRunPolicy,
/** slot name → resolved endpoint, filled by the Provisioner. */
endpoints: z.record(z.string(), z.string()).default({}),
/** slot name → { primary, fallback? } */
slotFills: z.record(z.string(), zSlotFill).optional(),
credentialRefs: z.record(z.string(), zCredRef).default({}),
/** The one hard bound. A run cannot outlive this. */
hardDeadline: zISO,
/** Checked at handshake; MAJOR only. */
contractVersion: zSemVer,
}).strict();Two slot maps exist because endpoints can only express one server per slot, which made
failover unrepresentable — a dead primary meant every tool call failed and the run merely
produced fewer findings. slotFills is optional, and endpoints remains the fallback
source, so an older Provisioner that fills only endpoints keeps working.
ctx.mcp() reads slotFills[slot] first, then endpoints[slot].
What your context sees:
| Manifest field | On HarnessContext |
|---|---|
runId, correlationId, binding, policy | same names |
parameters | ctx.input |
budget | wrapped as ctx.budget (a BudgetLedger) |
target | ctx.target — readonly |
hardDeadline | ctx.hardDeadline — readonly, ISO-8601 |
slotFills / endpoints | behind ctx.mcp(slot) |
composition.modelIds | behind ctx.model(role) |
credentialRefs, composition | not exposed |
credentialRefs remains unexposed, and that is coherent: a credRef is an indirection the
CredentialBroker resolves at injection time, so a harness holding the reference could do
nothing with it. What it needs arrives resolved, in the environment or in an input field.
zRun
export const zRunStatus = z.enum([
'queued', 'provisioning', 'running', 'paused_hitl', 'stopping',
'done', 'failed', 'canceled', 'timed_out',
]);There is no
budget_exceeded. Cost was removed as a terminal condition because a cost trip mid-scan severs the assessment and ships a client a partial report.timed_outis the bound that remains, and since spend accrues at a finite rate it bounds cost too.
export const zRun = z.object({
id: z.string().min(1),
correlationId: z.string().min(1),
serviceId: z.string().min(1),
binding: zRunBinding,
status: zRunStatus,
phase: z.string().optional(),
startedAt: zISO.optional(),
endedAt: zISO.optional(),
hardDeadline: zISO,
findingsCount: z.number().int().nonnegative().default(0),
spend: zSpend,
breaches: z.array(zThresholdBreach).default([]),
exitReason: z.string().min(1).optional(),
}).strict()
.refine((r) => !TERMINAL.has(r.status) || Boolean(r.exitReason), {
message: 'a terminal Run must record why it ended',
});Terminal is done | failed | canceled | timed_out, and every one of them must carry an
exitReason — v1 marked a no-op job done and stranded another in queued forever.
Domain objects
export const zRunBinding = z.object({
tenantId: z.string().min(1),
assessmentId: z.string().min(1),
assetId: z.string().min(1),
}).strict();The "who is this for" that every finding inherits. Resolved server-side with an uncached
read before a manifest is built — a write-path guard must never see a cached null.
export const zAssessmentType = z.enum(['web_vapt', 'infra', 'cspm', 'osint']);
export const zAssessmentStatus = z.enum(['scanning', 'scan_complete', 'in_review', 'published']);An Assessment is one type of testing against one asset, carrying a version history. A
retest adds a version; it does not create a new Assessment. currentVersion bumps only on
analyst publish, never on scan completion — so there is no v0, and an abandoned run leaves
no hole in the sequence.
in_review is the gate, not a formality. There is no status that skips it and no timeout
that publishes anyway.