AGENTS.md
A self-contained file for your repository that tells a coding agent how to build against these SDKs without inventing APIs — no access to the VERIFI monorepo required.
Copy this into AGENTS.md at the root of the repository where you are building a harness
or an MCP. It is written to be read by a coding agent, so it is terse, imperative, and
front-loads the refusals.
Trim the half you do not need — the harness section or the MCP section.
Why this file matters more than usual here
The failure mode this platform has repeatedly hit is code written against a guessed API
shape, with tests that passed because the fake agreed with the guess. Two shipped bugs came
from exactly that: a conformance probe that posted a plausible-looking
{"method":"list_tools"} which every real server answered 400 to, and a finding mapper that
never set impact or remediation because nobody checked what the field list actually was.
An agent with no grounding will invent a ctx.http(), a budget.canProceed(), or a
ctx.emitFinding({ evidence: [...] }), and all three look right. The rules below exist to
stop that.
The file
# AGENTS.md
## What this repository is
A VERIFI engine v2 component — a **harness** (TypeScript, drives an assessment) or an
**MCP** (Python, exposes tools to one). The normative reference is the developer
documentation at https://<your-docs-host>/ . Read the relevant section before writing code.
You do NOT have, and do not need, a checkout of the VERIFI platform repository. Everything
you need is in this repo's `node_modules`, on that site, or below.
## Hard rules
1. **Never invent an API.** For TypeScript, if you cannot point at the exported symbol in
`node_modules/@pragyacyber/harness-sdk/dist/index.d.ts` or
`node_modules/@pragyacyber/engine-contract/dist/index.d.ts`, it does not exist.
Read the `.d.ts`. Do not infer from the name. If the packages are not installed,
say so and stop — do not write code against a remembered shape.
2. **Never write a credential into a file, a log, a test fixture, or a sample** —
not even a fake-looking one. Credentials are indirections: `secret:<id>`,
`env:<VAR>`, or `bedrock-sts`. That pattern is enforced by the contract schema.
The registry token belongs in `$NODE_AUTH_TOKEN`, referenced from `.npmrc`, never
pasted into it.
3. **Never add anything that can refuse a spend.** `ctx.budget` meters and alerts.
There is no `canProceed`, no `enforce`, no cap. Time is the only hard bound.
4. **Never make a test pass by making the fake agree with the code.** A fake MCP
that answers whatever we send proves nothing. Test against the real protocol
or against a real container.
5. **Never report success without showing output.** See the last section.
6. **Do not commit, push, or deploy** unless explicitly told to in the moment.
## Getting the packages installed (TypeScript only)
The SDKs are PRIVATE packages in GitHub Packages under the `@pragyacyber` scope. They
are not on npmjs.com.
```
# .npmrc — checked in. The token is REFERENCED, never written here.
@pragyacyber:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
```
```bash
gh auth refresh -s read:packages # if gh is available
export NODE_AUTH_TOKEN=$(gh auth token)
npm view @pragyacyber/harness-sdk version # prove auth BEFORE writing code
```
If that returns `404`, the human's account has not been granted read access to the
package. That is not fixable from here. STOP and tell them to request access — for
their GitHub username, and separately for each `owner/repo` whose CI will install
(a manual grant on the package's "Manage Actions access" page, once per repository).
In a Dockerfile, pass the token as a BuildKit secret, NEVER a build ARG — an ARG is
readable in the image history and bakes the credential into every pulled layer:
```dockerfile
RUN --mount=type=secret,id=npm_token \
{ echo "@pragyacyber:registry=https://npm.pkg.github.com"; \
echo "//npm.pkg.github.com/:_authToken=$(cat /run/secrets/npm_token)"; } > .npmrc \
&& npm ci --omit=dev \
&& rm -f .npmrc
```
## Versions
`@pragyacyber/engine-contract@1.5.1` · `@pragyacyber/harness-sdk@1.2.0`.
Both are `1.x`: install with a CARET, `^1.0.0`. Do not pin exactly — that was a
workaround for `0.x` caret semantics and is no longer needed. Never end up with
two copies of `engine-contract` in one tree; check with
`npm ls @pragyacyber/engine-contract`.
## Verified facts about the SDKs
- `@pragyacyber/harness-sdk` runtime exports, in full:
`BudgetLedger`, `ConnectorVersionError`, `FailoverSlot`, `GrpcEngineChannel`,
`MODEL_PRICES_PER_MTOK`, `McpClient`, `ModelClient`, `ModelRoleNotBoundError`,
`SDK_VERSION`, `SlotNotFilledError`, `collectRunSecrets`, `createMcpResolver`,
`createModelResolver`, `credentialsFromEnv`, `defineHarness`, `i`, `isHarness`,
`loadEngineProto`, `priceFor`, `protoPath`, `redact`, `resolveAwsCredentials`,
`runHarness`, `zPlanDeclaration`. Everything else in the package is a type-only export.
- `isCompatible()` compares MAJOR only and returns false for anything unparseable.
- `defineHarness` throws if `models` is empty. Declare at least one role even if
the harness never calls `ctx.model()`; mark it `required: false`.
- `defineHarness` throws on a phase `order` that is not a POSITIVE INTEGER.
Number phases from 1. `0` and `1.5` both throw.
- `ctx.mcp(slot).call()` returns `{ ok, text, isError, latencyMs, error?, raw? }`.
It NEVER throws. A failed tool is `ok: false`.
- `ctx.mcp(slot).listTools()` DOES throw on a JSON-RPC error.
- `ctx.emitFinding(f)` is typed `RawFinding` from `@pragyacyber/engine-contract`.
The compiler checks the field names. It is validated again by the engine.
- `ctx.target` (a `TargetScope`) and `ctx.hardDeadline` (ISO-8601) ARE on the
context. `ctx.target.scope` is the authorized in-scope URL list — check against
it rather than trusting an input parameter. `ctx.credentialRefs` and
`ctx.composition` are NOT on the context.
- There is no `ctx.http`, no `ctx.fetch`, no way to get a raw client. That is
deliberate: every spend must pass the ledger.
- `ctx.model()` builds every request through the contract (`buildModelRequest`),
so every modelled provider works: `bedrock` signs with the run's task role;
`anthropic`, `openai_compat`, `nim` and `vertex` use the injected key. A failed
call returns `ok: false`, never throws. It DOES throw only when NO AWS credentials
resolve at all (normal locally). Fixed in harness-sdk 1.1.0.
- The harness never opens a listening port. It dials out, works, and exits. A
Dockerfile for one has no `EXPOSE` and no `HEALTHCHECK`.
## Harness rules
- Emit results as you produce them with `ctx.emitFinding`. Never batch to the end.
The engine persists each finding on arrival, so a run that dies late keeps
everything it already found.
- Give every emission a deterministic `findingKey`. No timestamps, no run ids, no
counters, no severity in the key. Shape: `<check-id>:<asset>`.
- Valid severities: `critical`, `high`, `medium`, `low`, `info`. `informational`
is REFUSED and the error names `info`. Field names that get silently ignored if
you guess: `cves` is PLURAL, `affected` is `{url, parameter}[]` not a string,
the stable id is `findingKey` not `dedupeKey`.
- Valid confidences: `confirmed`, `firm`, `tentative`. Default `firm`.
- Never emit `reviewStatus`, `publishedAt`, `reviewedBy` or `clientVisible`.
Both the schema and the engine refuse them. Review status is set by an analyst,
never by a harness.
- Always write `description`, `impact` and `remediation`. A bare tool string is
not a description.
- `declarePlan()` is a STATIC declaration, not the execution plan. Never describe
it as one.
- Clamp any tool timeout below 285s. `ctx.mcp().call()` aborts at 300s, and while
`createMcpResolver` accepts a `timeoutMs`, `runHarness` does not expose one —
so through the real entrypoint the ceiling stands.
- Pass `workspace: ctx.runId` to any shared execution MCP.
- A slot's first failed call latches it to its fallback for the rest of the run.
Do not write your own retry loop on top of that.
- Register the image by DIGEST. `imageDigest` must match
`/^sha256:[0-9a-f]{64}$/` — a tag is refused, and a harness with no digest can
never be probed.
## MCP rules (Python + FastMCP)
Write Python. `@pragyacyber/mcp-sdk`'s `buildMcpServer` serves a plain-JSON
`{method:'list_tools'}` envelope, NOT MCP JSON-RPC, so a server built on it
answers 400 to `initialize` and can never pass gate 3. Its `defineMcp` rules are
right and are the ones below.
- `GET /healthz` is liveness ONLY. It must touch no backend.
- `GET /test` must exercise the REAL backend and return 2xx with a JSON body
containing `ok: true`. Return 503 when unhealthy, so a caller checking only the
status code still gets the right answer.
- Execute your binaries to check them. `shutil.which` is not enough — a truncated
download resolves on PATH and fails on every call.
- Every tool doing blocking work must be `async def` + `anyio.to_thread.run_sync`.
A blocking sync tool freezes the event loop and the health probes with it, so the
registry marks the server unavailable during every scan it is working perfectly.
- At least one tool, or gate 3 can never pass.
- Every tool states whether it is destructive, in its docstring.
- Derive every path from a caller-supplied `workspace`. Guard against `..` and
absolute paths. Never fall back to `$HOME`.
- Bound timeouts, output size, and file transfer size. Clamp the caller's request
to the server's ceiling.
- Report every failure in the return value. Never return an empty success.
- Register as `sidecar`. `shared` is not even representable on a registration —
it requires a PASSED statelessness test, and declaring it is asserting an
outcome.
- A registration cannot carry `status`, `probe`, `probeInvoked`, `tools` or
`authRef`. Those are what a probe FOUND. The schemas are `.strict()`, so
sending one is a 400.
## Before you claim it works
- Harness: `npx tsc --noEmit`, then actually run it against a fake `EngineChannel`
and paste the output. State whether a finding was emitted and which events fired.
- MCP: run all three gates with curl against a live server and paste the output.
Count the tools in `tools/list` yourself.
- Never claim a probe passed without showing the response.
- A green dry run proves WIRING, not findings. Never report it as evidence the
scanner works.Grounding an agent that has no monorepo access
The reading order that used to appear here pointed at files inside the VERIFI platform repository. An external developer does not have those, so it has been replaced. There are three grounding sources, and an agent should be told to use them in this order:
- The installed type definitions.
node_modules/@pragyacyber/harness-sdk/dist/andnode_modules/@pragyacyber/engine-contract/dist/ship.d.tsfiles. These are the only fully authoritative source available locally, and they are on the developer's own disk. Reading them is not optional — rule 1 exists because an agent that skips this step invents plausible symbols. - This site. Point the agent at the pages, not the homepage:
/harness/define-harness/ what you declare, and what it validates
/harness/lifecycle/ what runs it, and in what order
/harness/tools/ the tool path, ToolResult, and slot failover
/harness/finding-stream/ what emitFinding accepts and how dedupeKey stays stable
/harness/budget/ metering, thresholds, and why nothing can refuse a spend
/harness/channel/ EngineChannel, and running without an engine
/harness/models/ ctx.model(), and how the SDK and contract share one wire format
/harness/packaging/ the container contract and how a harness is probed
/contract/finding-model/ the two-layer model — the shape emitFinding must satisfy
/contract/registries/ how a component reaches `available`
/contract/registration/ what a caller may send, and what is unrepresentable
/mcp/gates/ the conformance arbiter, gate by gate
/conformance/ what to run before you register anything
/gaps/ what is broken right now — read before designing around it- A live server, for MCP work. There is no
.d.tsequivalent for a Python MCP. The authoritative shape of a running server is what its owntools/listreturns over real JSON-RPC. An agent writing an MCP should be made to start it and probe it rather than reason about it.