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 (initialize →
notifications/initialized → tools/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:
| SDK | Registry |
|---|---|
version | semver |
slots.required / slots.optional | requiredSlots / optionalSlots |
models | modelRoles |
topology? (optional) | topology (required literal) |
inputs, declarePlan, run | absent 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. Engagement → Assessment 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.
| Was | Closed by |
|---|---|
emitFinding untyped, the real shape unexported in the engine, every author guessing or hand-copying | contract 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 threw | the 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 survive | contract 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 endpoint | src/registry/model-probe.ts: a real invocation, passing only on non-zero token counts |
| A Harness could not be probed at all — it serves nothing | src/registry/harness-probe.ts: image digest + scan freshness + severity, where unscanned fails like vulnerable |
Registration unvalidated — Record<string, unknown>, an id check, and as never | contract 1.4.0 added registration.ts; the route parses against it and 400s with the offending field |
A caller could assert status / probe / tools | the registration schemas are .strict() and simply do not carry outcome fields |
shared declarable at registration | zConcurrencyModel.exclude(['shared']) — not representable |
A harness registerable with no image digest, then stuck in probing forever | contract 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 unknown | contract 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 all | harness-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 model | harness-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 SDK | harness-sdk@1.0.0 collectRunSecrets(), wired in runHarness |
ctx withheld target and hardDeadline — a harness could not read its own authorized scope | both exposed on HarnessContext in harness-sdk@1.0.0 |
clientInfo.version hardcoded 0.3.0 while the package shipped 0.5.0 | SDK_VERSION, exported, currently '1.0.0' |
failover.test.ts with no failover.ts | FailoverSlot moved to src/failover.ts, re-exported from its old home |
Phase order: 0 valid in the SDK, invalid in the registry | defineHarness now requires a positive integer |
^0.x pinned the minor, so every consumer pinned exactly and SDK fixes stranded | harness-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
zHarnessRegistrationcarries none of them. - Whether any open item above is already fixed on an unmerged branch. Everything here was read from the working tree.