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

Contract overview and versioning

The zod schemas are the normative types. What the additive-only guarantee means, and exactly what you may rely on.


@pragyacyber/engine-contract is the only thing shared between the parts of the engine. Every other repository depends on this one, and on nothing else of ours.

                 ┌────────────────────────────────────────┐
                 │  @pragyacyber/engine-contract  1.x     │
                 └───────────────────┬────────────────────┘
                 ^1.0.0  ┌─────────┼──────────┬─────────────────┐
                         ▼         ▼          ▼                 ▼
                 verifi-engine  harness-sdk  mcp-sdk   your harness

Nothing in it executes. No I/O, no network, no database — zod schemas, the gRPC proto, the version constant, and a handful of pure helpers.

Installing

// package.json — a RANGE, never a pin
"dependencies": { "@pragyacyber/engine-contract": "^1.0.0" }
# .npmrc
@pragyacyber:registry=https://npm.pkg.github.com

Versioning — the rule the package exists to enforce

MAJOR.MINOR.PATCH, and additive-only within a MAJOR.

BumpMeaning
MAJORBreaking — a field removed, renamed or retyped. Requires migration.
MINORAdditive only — new optional fields, new event types. Backward-compatible by construction.
PATCHFixes and clarifications, no schema change.

npm run contract:diff enforces it in CI, comparing every exported schema against a checked-in contract-snapshot.json. A removal, rename or retype fails the build.

What the last few minors actually did

Each of these is a real change you may need to act on, and each is additive by the rule above:

VersionChange
1.5.1buildModelRequest resolves the Bedrock model family through a cross-region inference-profile prefix (us. us-gov. eu. apac. global.). The prefix stays in the invoke path and comes off before the family test. An unknown family is still refused.
1.5.0zHarnessRegistration.imageDigest — required, and must match /^sha256:[0-9a-f]{64}$/. A tag is refused.
1.4.3capabilities and costModel became optional on a model registration; invocationId added.
1.4.2Bedrock treated as a host, not a model family: amazon.nova* and amazon.titan-text* get their own body, and a family with no defined shape is refused rather than sent an Anthropic body.
1.4.1zRawSeverity relaxed to .passthrough() — it was stricter than the engine it describes.
1.4.0raw-finding.ts and registration.ts added; model-invocation.ts added; zEventType widened by six members.

The handshake checks MAJOR only

export const CONTRACT_VERSION = '1.5.1';

export function isCompatible(consumerVersion: string): boolean;

Verified behaviour:

isCompatible('1.0.0') -> true    // any 1.x is compatible with any other 1.x
isCompatible('1.5.1') -> true
isCompatible('2.0.0') -> false
isCompatible('1.0')   -> false   // malformed is NEVER "compatible"

That last row is deliberate. Never conflate "unparseable" with "compatible": treating a failed read as an absent value is how a live credential once got overwritten with a stale seed.

Because a major is additive-only, a newer engine understands every older minor of that major by construction — so within 1.x there is no support window to maintain, and every repo can depend on a caret range instead of a pin.

What you may rely on

Within 1.x:

  • Every field that exists today will still exist, with the same name and the same type.
  • Every enum member that exists today will still be accepted.
  • New optional fields may appear. Your .strict() parses of contract objects will not break, because you parse with the contract's own schemas.
  • New enum members may appear. Handle unknown members defensively — a switch over zEventType should have a default branch.

What you may not rely on:

  • That an enum will not gain members. zEventType, zAlertKey and zEvidenceKind are the likely growers.
  • That an optional field will stay absent.
  • That the engine's internal types match the contract. They mostly do; the ones that do not are listed in Known gaps.

Where .strict() appears, and why

Most object schemas are .strict(), so an unknown key is a parse error rather than a silently dropped field. Each one has a specific reason:

SchemaWhy strict
zBudgetSo a resurrected maxCostUsd or policy is a loud error rather than a field somebody believes is enforced.
zFindingSo a reviewStatus cannot be smuggled onto the immutable layer.
zAssessment, zRunBinding, …So a leftover engagementId from the pre-rename schema fails loudly instead of being quietly dropped.
zModel, zMcpServer, zHarnessSo a stored record cannot carry fields nobody validates.
zModelRegistration, zMcpRegistration, zKbRegistration, zHarnessRegistrationSo a caller cannot assert an outcome — status, probe, probeInvoked, tools, authRef. See Registering a component.

zSpend, zBudgetSlice and zThresholdBreach are not strict.

The one schema that is deliberately .passthrough()

zRawFinding — what a harness emits — allows and carries extra keys, because the engine does. A harness may attach whatever evidence it has, and the normaliser keeps what it recognises.

.strict() there would reject payloads the engine happily stores, which would make the schema a stricter authority than the system it describes. The nested zRawSeverity and zRawAffected are .passthrough() for the same reason: a recon harness legitimately carries cvssScore: null beside a severity label, deliberately, because a fabricated CVSS on a missing header looks authoritative and is worse than no score at all.

Being stricter than reality is not the safe direction. It is a different way to be wrong.

Refinements — the invariants that live in the schema

These are enforced by the type system, not by the code that writes the records. All four were verified by parsing.

SchemaRefusal
zModelRegistrationany of status, probe, probeInvoked, tools, authRef, lastProbedAt
zModelRegistrationa keyed provider with no apiKey; openai_compat/nim with no baseUrl; bedrock with no region
zMcpRegistrationneither endpoint nor image; concurrencyModel: 'shared'
zHarnessRegistrationan imageDigest that is a tag rather than sha256: + 64 hex
zRawFindingany of reviewStatus, publishedAt, reviewedBy, clientVisible
zModelstatus: 'available' without probeInvoked: true
zMcpServerstatus: 'available' without probe.passed === true
zMcpServerconcurrencyModel: 'shared' without statelessnessProbePassed
zKnowledgeBasestatus: 'available' with indexStats.chunks === 0
zProbeResultpassed: true unless all three gates are green and toolCount > 0
zRuna terminal status with no exitReason
zServicestatus: 'published' with an unbound required slot or model role
zServiceany KB binding with accessMode: 'read_write'
zPublishedFindinglastTestedVersion < introducedVersion
zBudget (zLevels)thresholds not strictly ascending
zTargetScopean empty scope array

Closed enums, and why

export const zSeverity = z.enum(['critical', 'high', 'medium', 'low', 'info']);
export const zConfidence = z.enum(['confirmed', 'firm', 'tentative']);
export const zModelTier = z.enum(['frontier', 'balanced', 'fast', 'local']);
export const zEnvironment = z.enum(['dev', 'staging', 'prod']);
export const zPlacement = z.enum(['shared', 'sidecar', 'warm_pool']);
export const zRedactionPolicy = z.enum(['off', 'standard', 'strict']);
export const zHitlMode = z.enum(['auto', 'approve_destructive', 'approve_all']);
export const zLifecycleStatus = z.enum([
  'unregistered', 'probing', 'available', 'degraded', 'unavailable', 'retired',
]);

An open string where a closed set belongs is how v1 ended up with "Critical" and "critical" as two different severities. Normalisation happens at the schema boundary or nowhere.

Note that zLifecycleStatus has no state meaning "registered, assumed working".

zCredRef — the credential rule as a type

export const zCredRef = z.string().regex(
  /^(secret:[A-Za-z0-9_-]+|env:[A-Z][A-Z0-9_]*|bedrock-sts)$/,
  'must be secret:<id>, env:<VAR>, or bedrock-sts — never a raw credential',
);

Three forms and nothing else. A raw connection string, API key or password is not representable anywhere a credRef is expected. The value is resolved server-side, in flight, and never persisted, logged, evented or returned by an API.

zISO

export const zISO = z.string().datetime({ offset: true });

ISO-8601 with an offset. A bare local timestamp is ambiguous across the two regions this platform runs in. new Date().toISOString() satisfies it.

zSemVer

export const zSemVer = z.string().regex(/^\d+\.\d+\.\d+$/, 'must be MAJOR.MINOR.PATCH with no prefix or range');

Three parts, no ranges, no v prefix.

Exported helpers

ExportWhat it does
CONTRACT_VERSION'1.5.1'. Must equal the package version; the release workflow asserts it.
isCompatible(v)MAJOR-only handshake check. Malformed → false.
computeDedupeKey(assetId, location, category)sha256 of the three, category lowercased.
crossedThresholds(levels, actual, alreadyFired)Newly crossed levels. Returns no decision.
ALERT_CATALOGUEDefault audience and urgency per alert key.
isKeyedProvider(provider)false for bedrock alone — it uses the AWS role.
modelEndpoint(provider, opts)The base URL. Refuses rather than defaults where a default would be dangerous.
buildModelRequest(provider, modelId, prompt, opts?){ path, body, headers }. Auth is the caller's job.
parseModelResponse(provider, payload){ text, inputTokens, outputTokens }. Zero tokens for an unrecognised shape, never a guess.
idempotencyKey, jitterOffsetSeconds, timeoutOutcomeScheduling and approval helpers, outside what a harness or MCP author needs. Listed so nobody assumes their absence.

The four model helpers are documented on Models. They are pure — no fetch, no credentials, no signing, no environment reads — because the engine signs with a task role and the SDK signs with per-run STS credentials, and baking either in would force one caller's auth model onto the other.

Also exported and normative, each with its own page: zRawFinding / the emit shape, and the four registration schemas / Registering a component.

residency.ts, schedule.ts and approval.ts are exported in full and are outside what a harness or MCP author needs; they are not documented here. contract-diff.ts backs the CI additive-only gate and is deliberately not exported from the package root — it is a build tool, not API.

The proto

"exports": { ".": "./dist/index.js", "./proto": "./proto/engine_channel.proto" }

The gRPC proto ships inside the contract package, so the client and server cannot diverge. protoPath() in the harness SDK resolves it.