{"_id":"@agent-orc/harness-protocol","_rev":"3-bfbed3aaf58209dadb4e5ae20b056b88","name":"@agent-orc/harness-protocol","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@agent-orc/harness-protocol","version":"1.0.0","keywords":["orca","harness","agent","mcp","ndjson"],"license":"MIT","_id":"@agent-orc/harness-protocol@1.0.0","maintainers":[{"name":"akashokik","email":"akash@okik.co.uk"}],"homepage":"https://github.com/okikorg/harness-protocol#readme","bugs":{"url":"https://github.com/okikorg/harness-protocol/issues"},"dist":{"shasum":"fc1d5decc74515d7a989b3d218f29b08cad8279f","tarball":"https://registry.npmjs.org/@agent-orc/harness-protocol/-/harness-protocol-1.0.0.tgz","fileCount":32,"integrity":"sha512-4BbLjeZgU6LcgoM4nySRwD2kmeOi4J6cQWn1WmqEpzRGpdlOCEP6bMtjAUN4cPcXiB4gkoRdWIAvUSkCnCiF+w==","signatures":[{"sig":"MEUCIQDH1sW8cldQn+cW5hYSiiDJcCb+BUV+mw0C40yrUiQXQQIgeR/lKDr5vXW3wLWTYncroaCNGFFMp7BOJ1TwwoukQYE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":108499},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"b75e2a86ac19dda8a9d1f963c2a4cbc4eb87c57c","scripts":{"test":"node --test 'test/*.test.ts'","build":"rm -rf dist && tsc -p tsconfig.json","prepack":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"akashokik","email":"akash@okik.co.uk"},"repository":{"url":"git+https://github.com/okikorg/harness-protocol.git","type":"git"},"_npmVersion":"11.6.0","description":"Orca Harness Protocol v1 server library: write the agent, not the wire","directories":{},"_nodeVersion":"24.9.0","dependencies":{"@modelcontextprotocol/sdk":"^1.30.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.0","@types/node":"^24.0.0"},"_npmOperationalInternal":{"tmp":"tmp/harness-protocol_1.0.0_1786638339963_0.9754945040354037","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@agent-orc/harness-protocol","version":"1.0.1","keywords":["orca","harness","agent","mcp","ndjson"],"license":"MIT","_id":"@agent-orc/harness-protocol@1.0.1","maintainers":[{"name":"akashokik","email":"akash@okik.co.uk"}],"homepage":"https://github.com/okikorg/harness-protocol#readme","bugs":{"url":"https://github.com/okikorg/harness-protocol/issues"},"dist":{"shasum":"704e25ac923e7e810c684295eedb4364c0979005","tarball":"https://registry.npmjs.org/@agent-orc/harness-protocol/-/harness-protocol-1.0.1.tgz","fileCount":32,"integrity":"sha512-8YwaRirHe/CGe3qMTTAJiPelwnRpWe4htZ2PO9xvffMBwMvs6WYsye2aZv5/+TXQmFX5NrKJE3tRyIeAS40ysw==","signatures":[{"sig":"MEYCIQCVnYqgn/ieiCbFUXuM14F9aiH9tBZVn9en/T23osxHygIhALThV7rlEI7aTPk+i1r5THBuWo1SRDJRffzHXw/tqUOr","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":108499},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"b75e2a86ac19dda8a9d1f963c2a4cbc4eb87c57c","scripts":{"test":"node --test 'test/*.test.ts'","build":"rm -rf dist && tsc -p tsconfig.json","prepack":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"akashokik","email":"akash@okik.co.uk"},"repository":{"url":"git+https://github.com/okikorg/harness-protocol.git","type":"git"},"_npmVersion":"11.6.0","description":"Orca Harness Protocol v1 server library: write the agent, not the wire","directories":{},"_nodeVersion":"24.9.0","dependencies":{"@modelcontextprotocol/sdk":"^1.30.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.0","@types/node":"^24.0.0"},"_npmOperationalInternal":{"tmp":"tmp/harness-protocol_1.0.1_1786638588134_0.8906134495763784","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@agent-orc/harness-protocol","version":"2.0.0","description":"Orca Harness Protocol v1 server library: write the agent, not the wire","license":"MIT","repository":{"type":"git","url":"git+https://github.com/okikorg/harness-protocol.git"},"homepage":"https://github.com/okikorg/harness-protocol#readme","bugs":{"url":"https://github.com/okikorg/harness-protocol/issues"},"type":"module","publishConfig":{"access":"public"},"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"engines":{"node":">=20"},"scripts":{"build":"rm -rf dist && tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","test":"node --test 'test/*.test.ts'","prepack":"npm run build"},"keywords":["orca","harness","agent","mcp","ndjson"],"dependencies":{"@modelcontextprotocol/sdk":"^1.30.0"},"devDependencies":{"@types/node":"^24.0.0","typescript":"^5.9.0"},"_id":"@agent-orc/harness-protocol@2.0.0","gitHead":"1e00cdcb495607e9730a07d3ef405a3c56f62297","_nodeVersion":"24.9.0","_npmVersion":"11.6.0","dist":{"integrity":"sha512-dUWcy8ZtLJqmA79+UAmU5BMpVXA98MFJa68nRwtXTjwLCi+ovtpEn8Oxm8ZJYfIEh1jZ99c1Na0omqqxOMgRjg==","shasum":"5e8644b2c64fc3a11edbf506b5c1371800bc5bef","tarball":"https://registry.npmjs.org/@agent-orc/harness-protocol/-/harness-protocol-2.0.0.tgz","fileCount":37,"unpackedSize":123134,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFnLZ6COyT/ijzfNnpNjFRY0YB+9TayD8Vd+N+SzNnawAiAOxjAMWnKSICGaCaSYOtRQaMhaz1EiaKjllHJrn+KLHA=="}]},"_npmUser":{"name":"akashokik","email":"akash@okik.co.uk"},"directories":{},"maintainers":[{"name":"akashokik","email":"akash@okik.co.uk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/harness-protocol_2.0.0_1786690336661_0.020037355481140606"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-13T16:25:39.671Z","modified":"2026-08-14T06:52:17.022Z","1.0.0":"2026-08-13T16:25:40.134Z","1.0.1":"2026-08-13T16:29:48.281Z","2.0.0":"2026-08-14T06:52:16.822Z"},"bugs":{"url":"https://github.com/okikorg/harness-protocol/issues"},"license":"MIT","homepage":"https://github.com/okikorg/harness-protocol#readme","keywords":["orca","harness","agent","mcp","ndjson"],"repository":{"type":"git","url":"git+https://github.com/okikorg/harness-protocol.git"},"description":"Orca Harness Protocol v1 server library: write the agent, not the wire","maintainers":[{"name":"akashokik","email":"akash@okik.co.uk"}],"readme":"# @agent-orc/harness-protocol\n\nA server library for [Orca Harness Protocol v1](https://github.com/okikorg/orca/blob/main/docs/harness-protocol/v1/spec.md).\n\nA harness is **your own agent-worker**. Orca's runtime (`agent-runtime`) boots\nit per session and drives it over exactly the contract the platform's built-in\nsidecar answers, so it drops into the same socket: same `/health`, `/healthz`,\n`/run`, `/state`, and `/models`.\n\nThis library is that contract. It handles routing, NDJSON framing, event\nordering, cancellation, request limits, state transfer, and connecting the\nsession's MCP tools. You write the agent loop.\n\nTerminology, because it is easy to get backwards: the **agent** is a profile you\nconfigure in Orca, and one harness serves many of them. The harness is the\nworker that runs them. There is only one runtime, and it is Orca's.\n\nIt is **not** a harness framework and **not** a set of agent-SDK adapters.\nThere is no `piAdapter()`. You import your SDK directly and map its events onto\n`ctx.emit`, which is about fifteen lines and leaves you in control of the loop.\n\n```\nYour SDK code  +  @agent-orc/harness-protocol  =  a conformant harness\n```\n\n## Install\n\n```bash\nnpm install @agent-orc/harness-protocol\n```\n\n## The shape\n\n```ts\nimport { createHarnessServer, type RunContext } from '@agent-orc/harness-protocol'\n\nconst harness = createHarnessServer({\n  // What this worker calls itself, reported as `runtime` in /health, exactly\n  // as the platform sidecar reports MODE=claude there. Not an agent name:\n  // agents are Orca profiles, and this one worker serves all of them.\n  harness: 'ledger-harness',\n\n  // Optional. Surfaces at GET /models, which is what fills the model picker\n  // in the Orca dashboard.\n  models: ['anthropic:claude-sonnet-4-5', 'openai:gpt-4o'],\n\n  async run(ctx: RunContext) {\n    ctx.emit.progress('reading the ledger')\n    ctx.emit.assistant('I found three unmatched lines.')\n    return 'Reconciled 3 of 3.'\n  },\n})\n\nawait harness.listen({ port: Number(process.env.PORT ?? 7099) })\n```\n\nThat is a conformant harness. Everything below is optional detail.\n\n## With an agent SDK\n\nPi, as a worked example. The pattern is the same for any SDK: create the\nsession, map the generic tools into whatever your SDK calls a tool, forward\ntext and usage to `ctx.emit`, and return the answer.\n\n```ts\nimport { createHarnessServer, type RunContext } from '@agent-orc/harness-protocol'\nimport {\n  createAgentSession,\n  ModelRuntime,\n  SessionManager,\n  type ToolDefinition,\n} from '@earendil-works/pi-coding-agent'\n\nconst models = await ModelRuntime.create()\n\nconst harness = createHarnessServer({\n  harness: 'ledger-harness',\n\n  async run(ctx: RunContext) {\n    // Already split, and leniently: an un-namespaced id leaves provider\n    // undefined, which is how claude and codex profiles are written.\n    const model = models.getModel(ctx.model.provider ?? 'anthropic', ctx.model.modelId)\n    if (!model) throw new Error(`unknown model: ${ctx.model.id}`)\n\n    // ctx.tools is generic. This is where a Pi harness makes it Pi's. Pi types\n    // parameters as a TypeBox TSchema (a JSON Schema object at runtime) and\n    // wants a content array back rather than a bare value.\n    const customTools: ToolDefinition[] = ctx.tools.map((tool) => ({\n      name: tool.name,\n      label: tool.name,\n      description: tool.description,\n      parameters: tool.inputSchema as unknown as ToolDefinition['parameters'],\n      async execute(toolCallId: string, params: unknown) {\n        const output = await tool.call(params, { toolCallId })\n        return {\n          content: [{ type: 'text' as const, text: String(output) }],\n          details: undefined,\n        }\n      },\n    }))\n\n    const { session } = await createAgentSession({\n      model,\n      modelRuntime: models,\n      sessionManager: SessionManager.inMemory(),\n      tools: customTools.map((t) => t.name),\n      customTools,\n    })\n\n    ctx.signal.addEventListener('abort', () => void session.abort(), { once: true })\n\n    session.subscribe((event) => {\n      if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {\n        ctx.emit.assistant(event.assistantMessageEvent.delta)\n      }\n      if (event.type === 'message_end' && event.message?.usage) {\n        ctx.emit.usage({\n          inputTokens: event.message.usage.input ?? 0,\n          outputTokens: event.message.usage.output ?? 0,\n        })\n      }\n    })\n\n    await session.prompt(ctx.subtask.prompt)\n\n    return {\n      message: finalText(session.agent.state.messages),\n      state: { messages: session.agent.state.messages },\n    }\n  },\n})\n\nawait harness.listen({ port: Number(process.env.PORT ?? 7099) })\n```\n\n`orca harness init --sdk pi` writes a working version of this.\n\n## What you get\n\n### `ctx`\n\n| Field | What it is |\n|---|---|\n| `ctx.subtask.prompt` | The work to do. |\n| `ctx.model` | `profile.model` split into `{ id, provider?, modelId, supported }`. |\n| `ctx.profile` | `name`, `runtime`, `model`, `systemPrompt`, `tools`, `template`. |\n| `ctx.tools` | Platform and user MCP tools, already connected. Always an array. |\n| `ctx.skills` | Resolved skill documents. Use these; never read the host filesystem. |\n| `ctx.state` | Whatever the previous run returned as `state`, or `undefined`. |\n| `ctx.signal` | Aborts when the platform cancels. Thread it into model and tool calls. |\n| `ctx.emit` | `progress`, `assistant`, `usage`, `session`, `toolCall`, `toolResult`. |\n| `ctx.request` | The raw envelope, for anything this version does not surface. |\n\n### `ctx.tools`\n\nEvery MCP server in the envelope is connected before `run` is called, and their\ncatalogs are flattened into one list. Names are prefixed `mcp__<server>__` so\ntwo servers exporting `search` cannot collide.\n\n```ts\ntype HarnessTool = {\n  name: string\n  description: string\n  inputSchema: Record<string, unknown>   // JSON Schema\n  call(input?: unknown, opts?: { toolCallId?: string }): Promise<unknown>\n}\n```\n\n`call` emits a `tool_call` before and a `tool_result` after, so tool use shows\nup in the Orca transcript without you reporting it. It resolves with structured\noutput when the tool provides it, otherwise the joined text.\n\nPass `tools: false` to skip connecting, if your harness brings its own.\n\n### Returning\n\n```ts\nreturn 'the answer'                                  // stateless\nreturn { message: 'the answer', state: { ... } }     // stateful\n```\n\nThe `state` value comes back as `ctx.state` on the next run for that session,\nincluding after Orca has moved the session to a different replica. It must be\nJSON-serializable and under 8 MB.\n\n### Models\n\n`models` is declarative. It answers `GET /models` in the shape the conductor\ndecodes, which is how a tenant's models reach the dashboard picker:\n\n```json\n{ \"runtimes\": { \"ledger-harness\": [\"anthropic:claude-sonnet-4-5\"] }, \"errors\": {} }\n```\n\nDeclare nothing and the route does not exist, which is the sidecar's own\nconvention: the conductor drops an upstream it cannot reach, where an empty\n`200` would be taken as authoritative and blank the picker.\n\nThe library never refuses a run over it. Orca does not validate the model on a\ncustom-runtime profile either, so rejecting one here would make your harness\nstricter than the worker it replaces and fail runs the profile was legal for.\n`ctx.model.supported` tells you; what to do is yours:\n\n```ts\nif (!ctx.model.supported) ctx.emit.progress(`${ctx.model.id} is untested here`)\n```\n\n## What the library guarantees\n\nThese are the parts of the spec that are easy to get wrong by hand, and the\nreason this exists rather than a copy-pasted `server.ts`:\n\n- Exactly one terminal event per run, always last. Anything emitted after it is\n  dropped rather than corrupting the stream.\n- A client disconnect aborts `ctx.signal` and does not take the process down.\n  A run that finishes normally does **not** abort, which is the bug most\n  hand-written harnesses ship.\n- A malformed envelope is a `400` before the stream opens, never an error event\n  on a `200`.\n- `GET /state/{id}` answers `404` when nothing is stored. That is the correct\n  stateless answer, not a failure.\n- Request bodies and state bundles are capped rather than buffered without\n  limit, and `/health` stays under the 64 KB the platform reads.\n- MCP servers are closed when the run ends, including when connecting one of\n  them failed part-way through.\n- `/healthz` answers as well as `/health`, and `/models` appears only when you\n  declare models, both matching the platform sidecar.\n\n## Security\n\nThe platform never sends its own credentials to a harness. The only authority a\nrun carries is the session-scoped MCP endpoint in its envelope. Model provider\ncredentials belong to you, and reach the harness through its own environment.\n\nTenant-declared MCP server URLs get a scheme and literal-address check before\nconnect: loopback, RFC1918, link-local, and the cloud metadata address are\nrefused. This is defense in depth, not the authority, because it runs before\nDNS resolution. Set `HARNESS_ALLOW_HOSTS=localhost` for local development.\n\n## Conformance\n\n```bash\nnode index.ts &\n./conformance.sh http://localhost:7099\n```\n\nThe checker lives in the platform repo at `examples/brains/conformance.sh`. It\nexercises the wire behaviors a unit test cannot see. This library passes all 15\nchecks with no skips.\n\n## Versioning\n\nThe package version and the wire version are independent. `@agent-orc/harness-protocol@2`\nmay still speak `orca-harness/v1`. Do not infer protocol compatibility from an\nnpm version.\n","readmeFilename":"README.md"}