Packaging and boot
The container contract — what the Provisioner supplies, what the image must not contain, and how the entrypoint wires it up.
A harness container serves nothing. It dials out, does its work, and exits. One container per run, torn down on settle.
That is why McpSpec.serves is false for a harness and why a harness can never pass an
endpoint-based probe — there is no endpoint. Registration of a harness is by image digest,
not by URL.
The entrypoint
This is the production ASM harness's main, unabridged in substance:
import type { RunManifest } from '@pragyacyber/engine-contract';
import { runHarness, GrpcEngineChannel } from '@pragyacyber/harness-sdk';
import { myHarness } from './harness.js';
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required but not set — the Provisioner supplies it`);
return value;
}
async function main(): Promise<void> {
const manifest = JSON.parse(required('RUN_MANIFEST')) as RunManifest;
const runToken = required('RUN_TOKEN');
const grpcAddr = required('ENGINE_GRPC_ADDR');
// The stable internal ALB base, used to re-discover a live engine and reconnect if
// this engine is replaced mid-run. Optional: without it a dropped stream cannot recover.
const httpAddr = process.env['ENGINE_HTTP_ADDR'];
const channel = new GrpcEngineChannel({
grpcAddr,
runId: manifest.runId,
correlationId: manifest.correlationId,
runToken,
keepaliveMs: 30_000,
...(httpAddr ? { httpAddr } : {}),
});
const outcome = await runHarness(myHarness, manifest, channel);
// Non-zero exit for a failed run, so the container's own status agrees with what it
// reported over the wire.
process.exit(outcome.status === 'done' ? 0 : 1);
}
main().catch((err) => {
console.error(`failed to start: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});The environment contract
| Variable | Required | What it is |
|---|---|---|
RUN_MANIFEST | yes | The full RunManifest as JSON. |
RUN_TOKEN | yes | Single-run bearer token, expires at hardDeadline. Never log it. |
ENGINE_GRPC_ADDR | yes | Where to dial the engine's gRPC endpoint. |
ENGINE_HTTP_ADDR | no, but set it | The stable internal ALB base. Without it a dropped stream cannot recover from a task replacement. |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN | only if you call ctx.model() | Short-lived, minted per run. Read by credentialsFromEnv(). |
AWS_REGION | no | Defaults to us-east-1 in runHarness. |
The image
The ASM harness's Dockerfile is the reference. Three decisions in it are load-bearing:
Ship lean. Carry no security tools. Every tool runs on an MCP over a metered slot, so the image is just Node plus the harness. This is what puts tool calls back on the ledger — in-process spawning silently bypassed it.
Non-root. A scanner harness needs no privileges of its own.
No HEALTHCHECK, no EXPOSE. The container serves nothing.
FROM node:22-slim AS deps
WORKDIR /app
# The registry token arrives as a BuildKit SECRET, never an ARG — an ARG is readable
# in the image history, which would bake a credential into every pulled layer.
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=secret,id=npm_token \
corepack enable && \
{ echo "@pragyacyber:registry=https://npm.pkg.github.com"; \
echo "//npm.pkg.github.com/:_authToken=$(cat /run/secrets/npm_token)"; } > .npmrc && \
pnpm install --prod --frozen-lockfile && \
rm -f .npmrc
FROM node:22-slim AS runtime
WORKDIR /app
RUN groupadd -r verifi && useradd -r -g verifi verifi
COPY --from=deps --chown=verifi:verifi /app/node_modules ./node_modules
COPY --chown=verifi:verifi package.json .npmrc ./
COPY --chown=verifi:verifi src ./src
RUN mkdir -p /app/reports && chown verifi:verifi /app /app/reports
USER verifi
ENV NODE_ENV=production
CMD ["node", "--import", "tsx", "src/main.ts"]The mkdir -p /app/reports && chown line exists because the WORKDIR is root-owned, so a
non-root user cannot create directories in it. If your harness writes anything locally,
pre-create and chown the directory.
Registration
An admin registers the built image by digest — not by tag.
export const zHarnessRegistration = z.object({
id: z.string().min(1),
name: z.string().min(1),
/** The ECS task-definition family or image. A harness SERVES NOTHING, so no endpoint. */
image: z.string().min(1),
/** REQUIRED. A DIGEST, not a tag — a tag can be repointed after the probe passes. */
imageDigest: z.string().regex(/^sha256:[0-9a-f]{64}$/, 'must be a sha256 digest, not a tag'),
semver: z.string().min(1).optional(),
}).strict();Get the digest from the registry after a push, never from your local tag:
aws ecr describe-images --repository-name my-harness \
--image-ids imageTag=v1.2.0 --query 'imageDetails[0].imageDigest' --output textzCompositionSnapshot.harnessDigest carries the same value into every manifest, for the
same reason: "what exactly ran" has to be answerable months later for audit, and a tag is a
moving target.
How a harness is probed
A harness serves nothing — it boots from a manifest, dials out over gRPC, works and
exits. It has no /healthz and no port, so the three-gate HTTP probe is a category error
for it, and giving the harnesses registry that probe is why a harness could never reach
available.
What is verifiable is the artefact. The harness probe checks:
| Check | Failure |
|---|---|
an imageDigest is registered | no image digest registered — there is nothing to verify (a harness serves no endpoint to probe) |
| the image scanner is reachable | the image scanner could not be reached: … |
| the image has been scanned | this image has never been scanned — "no findings" and "no scan" are different statements, and an unscanned image is not evidence of a safe one |
| the scan is not stale (default: 30 days) | the last scan is N days old — a CVE published since then is invisible to it |
no finding at a blocking severity (default: critical, high) | the offending severities |
That claim matters because a harness runs inside a client's blast radius with brokered credentials. Build lean, keep the base image current, and re-push before the scan ages out.
zHarness (the stored registry record) is a richer shape than both HarnessRegistration
and the SDK's HarnessDefinition, and nothing generates one from the other — see
Registry objects and
Registering a component.
Resource declarations
resources: { cpu: '2', memoryMb: 4096, maxRuntimeMinutes: 1440 },Set maxRuntimeMinutes honestly. A full-surface sweep with heavy tools legitimately runs
for hours, and time is the only ceiling. Because findings stream out as they are produced,
a long run is safe — a run that is cut short still keeps everything it found.