VERIFISDK docsengine v2 · contract 1.5.1 · harness-sdk 1.2.0
Appendix

Known gaps

Places where the SDKs are contradictory, underspecified, or missing something a newcomer will reach for — and what the last two releases fixed.


This page exists because a documentation site that hides the rough edges makes them cost more, not less. Everything here was found by reading source and, where the entry says verified, by running it.

Reviewed 2026-08-18 against @pragyacyber/engine-contract@1.5.1, @pragyacyber/harness-sdk@1.2.0 and @pragyacyber/mcp-sdk@0.1.0.

Thirteen of the seventeen gaps this page listed in its first edition are now closed. They are recorded in What was fixed rather than deleted, because knowing a trap existed is how you recognise its shape when it reappears somewhere else.

Open

1. @pragyacyber/mcp-sdk's server does not speak MCP

Severity: real bug. Do not deploy buildMcpServer.

buildMcpServer's POST /mcp dispatches on { method: 'list_tools' | 'call_tool' } — plain JSON, not JSON-RPC 2.0. The engine's three-gate probe and the harness SDK's McpClient both send real MCP: initialize, notifications/initialized, tools/list, tools/call.

Verified by building one and probing it both ways:

GET  /healthz                                     200
GET  /test                                        200
POST /mcp {"method":"list_tools"}                 200  {"tools":[…]}
POST /mcp {"jsonrpc":"2.0","method":"initialize"} 400  {"ok":false,"error":"unknown method 'initialize'"}
POST /mcp {"jsonrpc":"2.0","method":"tools/list"} 400  {"ok":false,"error":"unknown method 'tools/list'"}

So a server built with it passes gates 1 and 2, fails gate 3, and no harness can call it. It is the same mistake gate 3 itself once made in the other direction — a probe written against a guessed protocol that every real server answered 400 to. That one was caught by running it against a live container; this one survives because it has never been run against anything. A component wired to nothing still passes all of its tests.

defineMcp's authoring-time refusals are correct and worth copying; they are taught on the MCP pages.

Fix shape: replace the handler with real JSON-RPC (initializenotifications/initializedtools/list / tools/call), SSE-capable, honouring mcp-session-id and both Accept types — or drop buildMcpServer and keep defineMcp + probeStatelessness as the authoring half only.

2. The MCP call timeout is not reachable from runHarness

Severity: moderate. Partially fixed.

McpClientOptions.timeoutMs is now a real option and createMcpResolver threads it through — so a harness that builds its own resolver can raise the 300 s default.

But runHarness(harness, manifest, channel, opts) has no timeoutMs in opts, and runHarness is what builds the resolver behind ctx.mcp(). Through the normal container entrypoint the ceiling is still 300 s with no hook, which is why the production ASM harness clamps every command to 285 s server-side.

Fix shape: an mcpTimeoutMs in runHarness's opts, or better, a manifest field so an operator can raise it for a slow estate without a redeploy.

3. Evidence has no author-facing path

Severity: moderate. Unchanged.

zEvidence is fully specified — content-addressed by sha256, with a storageRef and a redacted flag — and zFinding.evidenceIds exists. But emitFinding has no evidence parameter and the normaliser sets evidenceIds: [] unconditionally. The same is true of reproSteps and references, and of location.port, location.method and cvss: all present in zFinding, none reachable from what a harness emits.

For a security platform, "here is the HTTP transcript that proves it" is not a nice-to-have. Note location.port in particular — the contract keeps it explicitly because v1 stripped ports and rescanned :8443 as :443, and a harness still cannot set it.

You can attach evidence informally: zRawFinding is .passthrough(), so an extra key survives the schema. It will not be normalised into an Evidence record.

4. The HITL approval token has no transport to an MCP

Severity: unresolved design question. Unchanged.

The rule appears in three places — a destructive tool requires an approved HITL token in the CallContext. But CallContext exists only in mcp-sdk, whose server should not be deployed; neither asm/server.py nor kali/server.py accepts or checks a token; and McpSlot.call(tool, args) has no way to pass one out of band. The proto carries ApprovalRequest / ApprovalDecision between the harness and the engine, not between the harness and an MCP.

So the enforcement point is currently the harness, on the honour system. If you are writing a destructive tool, ask rather than building against an assumed mechanism.

5. No over-the-wire statelessness runner

Severity: moderate. Unchanged.

probeStatelessness(mcp, opts) takes an in-process TypeScript Mcp object and calls tool.handler directly. Every MCP that exists is Python, so the one check gating shared — whose failure mode at shareScope: 'global' is a cross-tenant data leak — has no runnable implementation for any server in production.

The engine's mcp-live.e2e.test.ts exercises the three gates against a real container; that is a different check. Write the interleaved two-session test yourself and say how you tested it. The design is documented.

Note the schema does its half correctly: zMcpRegistration refuses shared outright, so nothing can be registered as shared on an assertion. The gap is that nothing can earn it automatically either.

6. No bridge from HarnessDefinition to zHarness

Severity: moderate. Partially fixed.

What you write in defineHarness and what gets registered are still different objects with different field names, and nothing converts one to the other:

SDKRegistry
versionsemver
slots.required / slots.optionalrequiredSlots / optionalSlots
modelsmodelRoles
topology? (optional)topology (required literal)
inputs, declarePlan, runabsent entirely

The live trap inside it is fixed: defineHarness now validates phase order as a positive integer, matching zPhase, so numbering from zero fails on your machine rather than at registration.

Also note zHarnessRegistration — what a caller actually POSTs — is much narrower than zHarness, carrying only id, name, image, imageDigest and an optional semver. Everything else on the stored record has to arrive some other way, and how a harness's declared slots and model roles reach the registry is not something this site could verify.

Fix shape: a toRegistryHarness(def, { id, imageDigest }) helper in the SDK.

7. The conformance runner is not a runner you can run

Severity: moderate. Unchanged.

threeGateProbe is the correct arbiter and is genuinely language-neutral, but it is a function inside the engine repository — not a published package, not a CLI. An external MCP author cannot npx it. The two options today are hand-rolled curl (documented on Conformance) or registering and using the engine's probe route.

8. EngagementAssessment is renamed in the contract only

Severity: known and tracked. Unchanged.

The contract calls it Assessment. The product UI and the production databases still carry engagementId. The schemas are .strict() so a stray engagementId fails loudly, which is the right handling — but a newcomer reading both will not know which is current. The contract is the target state.

9. ctx.policy is delivered and enforced by nobody

Severity: low, but easy to misread.

ctx.policy.allowedActions / deniedActions are free strings the SDK never reads. ctx.policy.redaction likewise: the SDK's own scrub is collectRunSecrets + redact, which does not consult it. If your harness takes actions a rule of engagement could forbid, you must honour it yourself.

What was fixed

Kept for shape recognition. Each entry says what closed it.

WasClosed by
emitFinding untyped, the real shape unexported in the engine, every author guessing or hand-copyingcontract 1.4.0 published zRawFinding; harness-sdk@1.0.0 types the parameter RawFinding
Informational findings unstorable on either spelling — the engine's allow-list said informational, zSeverity said info, both threwthe allow-list is now new Set(zSeverity.options), derived rather than restated; near misses are refused with the correct value named
zEventType closed while the SDK emitted six types not in it — so the events describing a degraded run were the least likely to survivecontract 1.4.0 widened the enum by exactly those six
A Model could never reach available — all four registries shared one HTTP probe and a Bedrock model has no endpointsrc/registry/model-probe.ts: a real invocation, passing only on non-zero token counts
A Harness could not be probed at all — it serves nothingsrc/registry/harness-probe.ts: image digest + scan freshness + severity, where unscanned fails like vulnerable
Registration unvalidatedRecord<string, unknown>, an id check, and as nevercontract 1.4.0 added registration.ts; the route parses against it and 400s with the offending field
A caller could assert status / probe / toolsthe registration schemas are .strict() and simply do not carry outcome fields
shared declarable at registrationzConcurrencyModel.exclude(['shared']) — not representable
A harness registerable with no image digest, then stuck in probing forevercontract 1.5.0 made imageDigest required and sha256:-shaped
Anthropic models on Bedrock non-selectable — the family test ran against the raw id, so us.anthropic.… matched nothing and every modern model was refused as unknowncontract 1.5.1 strips the inference-profile prefix before the family test and keeps it in the path
ctx.model() shaped an Anthropic body for every provider — a Nova model passed its probe then 400'd on every real call, and non-bedrock providers were unreachable from a harness at allharness-sdk@1.1.0 routes ctx.model() through the contract's buildModelRequest / parseModelResponse / modelEndpoint; it also resolves Fargate task-role credentials (so ctx.model() no longer threw on every real run) and double-encodes the SigV4 canonical URI
A keyed provider role was still built as bedrock from a harness — 1.1.0 fixed the wire format and could attach a key, but createModelResolver never set a role's provider/baseUrl/apiKey, so a NIM / OpenAI-compatible sub-agent defaulted to Bedrock, failed buildModelRequest, and was silently unusable; a service mixing a Bedrock lead with a keyed sub-agent could not run the second modelharness-sdk@1.2.0 reads a per-role VERIFI_MODEL_<ROLE>_PROVIDER / _BASEURL / _APIKEY (injected by the engine from its registry + Secrets Manager, the key landing only in the container env) and passes them to the ModelClient; absent env still falls back to Bedrock. Read from source — a live end-to-end NIM run is still pending. See Models
secrets redaction never populated — declared, typed, read on every call, and fed an empty list for the life of the SDKharness-sdk@1.0.0 collectRunSecrets(), wired in runHarness
ctx withheld target and hardDeadline — a harness could not read its own authorized scopeboth exposed on HarnessContext in harness-sdk@1.0.0
clientInfo.version hardcoded 0.3.0 while the package shipped 0.5.0SDK_VERSION, exported, currently '1.0.0'
failover.test.ts with no failover.tsFailoverSlot moved to src/failover.ts, re-exported from its old home
Phase order: 0 valid in the SDK, invalid in the registrydefineHarness now requires a positive integer
^0.x pinned the minor, so every consumer pinned exactly and SDK fixes strandedharness-sdk 1.0.0

What this site still could not verify

  • The engine-side registration and Service composition flow end to end. The routes and schemas were read and the schemas were exercised; no registration was run against a live engine.
  • How a harness's declared slots, model roles and phases reach the registry, given that zHarnessRegistration carries none of them.
  • Whether any open item above is already fixed on an unmerged branch. Everything here was read from the working tree.