---
title: "Build a runtime"
description: "Start a runtime on the reference SDK or in any language, meet the ten-point contract, and keep catalog, hub and RBAC private behind the open pack format."
canonical: https://harnesscontextprotocol.io/docs/runtimes
---

# Build a runtime

> Start a runtime on the reference SDK or in any language, meet the ten-point contract, and keep catalog, hub and RBAC private behind the open pack format.

In HCP the harness is the runtime. Whatever already drives the agent loads
packs, enforces grants, mints Binding and runs Adapters, so there is no separate
server to host and no daemon to keep alive. Start on the reference TypeScript
SDK below, or build in any language against [the contract](#the-contract) and
prove it with the [conformance vectors](/docs/runtimes/conformance).

## Start with the reference SDK

The reference runtime is `@harnesscontextprotocol/runtime`, the `Hcp` class
(also at `@harnesscontextprotocol/sdk/runtime`). It is file-backed, in-process
and local, takes pack directories, a Principal and grants with nothing else
implicit, and exists so the protocol runs from day one and the conformance
vectors have something to run against.

<Steps>
<Step>

### Install

The packages are in a private preview on GitHub Packages. With preview access,
point the scope there; `GITHUB_TOKEN` is a token that can read packages:

```text title=".npmrc"
@harnesscontextprotocol:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
```

```bash
bun add @harnesscontextprotocol/runtime
bun add @harnesscontextprotocol/sdk
bun add -g @harnesscontextprotocol/cli
```

A runtime that only parses and lints packs, with no agent loop, can import
`@harnesscontextprotocol/sdk/pack`, which carries no AI SDK dependency.

</Step>
<Step>

### Create a runtime over pack directories

```ts title="runtime.ts"
import { Hcp } from '@harnesscontextprotocol/runtime'

const hcp = await Hcp.create({
  packDirs: ['./packs/transit', './packs/ledger'],
  sandbox: true,                        // default posture
  allowBins: ['transit', 'ledger'],     // wrap bins the sandbox may spawn
  principal: { id: 'user_42', kind: 'human' },
  tenant_id: 'acme',
  project_id: null,                     // required only for land: brain
  grants: [
    { capability: 'verb.exec.read',  pack_id: 'transit' },
    { capability: 'verb.exec.read',  pack_id: 'ledger' },
    { capability: 'verb.exec.write', pack_id: 'ledger', verb_keys: ['transfer'] },
    { capability: 'pack.connect',    pack_id: 'ledger' },
  ],
})
```

Leave out `grants` only in a local demo ([Authorization](/docs/concepts/access#authorization)).

</Step>
<Step>

### Activate, connect, exec

```ts
await hcp.activate(['transit', 'ledger'])
await hcp.connect('ledger')                    // Binding mint; skipped for public packs

const read = await hcp.exec('transit', 'plan', { from: 'A', to: 'B' })
// { ok: true, pack_id: 'transit', verb_key: 'plan', result: {…} }

const denied = await hcp.exec('ledger', 'transfer', { amount: 10 })
// { ok: false, error: 'confirm required' }

const ok = await hcp.exec('ledger', 'transfer', { amount: 10, confirm: true })
// { ok: true, … }
```

Every result is an envelope, `{ ok, pack_id, verb_key, result?, error? }`, and
denials come back as values an agent can branch on, never as thrown exceptions.

</Step>
<Step>

### Report availability

```ts
import { buildPackAvailability } from '@harnesscontextprotocol/sdk'

for (const pack of await hcp.listPacks()) {
  const report = buildPackAvailability({
    pack,
    installed: true,
    activated: hcp.activatedPacks().includes(pack.pack_id),
    connected: false,
    grants: hcp.authz.grants,
  })
  // report.verbs[i] → { verb_key, operation, land, executable, reason? }
}
```

An agent reads [availability](/docs/concepts/lifecycle#availability) before it
plans, so it does not discover a denial mid-run.

</Step>
</Steps>

## The contract

A conforming runtime does these ten things, each in whatever way it chooses.
Every exec passes them in the order of the
[pipeline](/docs/runtimes/execution#the-pipeline), and any step may deny.

| # | Owns | Fail-closed rule | Page |
|---|------|------------------|------|
| 1 | Load `surface.json`, `policy.json` and `HARNESS.md` from a pack directory | Reject on parse error | [Execution](/docs/runtimes/execution) |
| 2 | Lint on install: Policy covers every command and trigger, Skill present, wrap has named commands or `pass_through` | Reject on any issue | [Testing](/docs/packs/check#testing) |
| 3 | Authorise each exec against five capability axes for a Principal | Deny with a reason; never default-allow | [Authorization](/docs/concepts/access#authorization) |
| 4 | Gate `confirm: true` writes | Refuse before any adapter runs | [Execution](/docs/runtimes/execution) |
| 5 | Mint Binding through a hub for `user`, `local` and `mothership` | No ambient env fallback | [Binding](/docs/concepts/binding) |
| 6 | Execute the Adapter: spawn the wrap bin with a rendered argv, or call in-process | Unknown bin denied | [Execution](/docs/runtimes/execution) |
| 7 | Sandbox by default: bin allow-list, cwd jail, no inherited credentials | `native-local` only by explicit opt-in | [Sandbox](/docs/runtimes/sandbox) |
| 8 | Audit writes | Full attribution tuple | [Audit](/docs/concepts/access#audit) |
| 9 | Report availability per command: installed, activated, connected, executable | Every `false` carries a reason | [Lifecycle](/docs/concepts/lifecycle) |
| 10 | Route Sense (optional): accept declared triggers and record them in a sink | `sense.enable` off → drop | [Sense delivery](/docs/concepts/sense#sense-delivery) |

A runtime implements the protocol, stores and runs packs, and enforces their
Policy. It must not:

- Add its own Policy rows, override a pack's, or rewrite a pack.
- Extend the protocol's types (a "Connector", another land, a new capability)
  without an RFC.
- Fall back to environment tokens when mint fails.
- Collapse read, write and connect into one boolean in a multi-tenant product.
- Let an end-user Principal reach a `mothership` Binding without an explicit
  elevation grant.
- Treat polling as Sense, or deliver undeclared triggers.

## Drivers

`Hcp` is a thin front over an `HcpRuntimeDriver`, a small interface with
`listPacks`, `getPack`, `exec` and an optional `connect`. Two drivers ship, and
your own implements the same interface:

| Driver | Purpose |
|--------|---------|
| `reference-local` | File-backed, in-process, sandbox allow-list, default authoriser. The conformance target |
| `proprietary-stub` | In-memory catalog and grants (`createProprietaryStubRuntime`). The shape of a company runtime, for tests |

## Proprietary runtimes

The pack format is open and a runtime does not have to be, so a company can put
HCP in a real product without publishing its internals while its packs stay
portable. HCP needs no public network "HCP server", because remote transport is
a runtime choice and the protocol defines no server role, and no registry, since
a local directory install is enough for HCP/1.x. Harnesses see only `activate`,
`connect`, `exec`, the Skill and optional Sense.

| You keep private | Freedom |
|------------------|---------|
| Pack catalog | Private, curated, versioned however you like: SQL tables, a directory, a private registry |
| Binding hub | Your keyring, OAuth flows and refresh schedule, a local file store, an OS keychain |
| RBAC | Your role tables, row-level security, IdP and tenancy model, mapped onto the five capabilities |
| Sandbox depth | Process allow-list up to containers and microVMs |
| Sense sink | An inbox, a queue, a webhook fan-out, UI badges |
| Principal mapping | Session → `{ id, kind, tenant, project }` |
| Transport | In-process, HTTP, stdio, or none |

None of these is an HCP type, and you never publish ACL tables, catalog schema
or hub internals. The protocol fixes what keeps packs portable:

| Fixed by the protocol | Why |
|-----------------------|-----|
| The three files and their schemas | So packs are portable |
| Command twin face and `verb_key` | So invokers are interchangeable |
| Policy tokens and the confirm gate | So authors write one Policy |
| Binding modes and hubs (as names) | So packs declare an expectation without knowing your hub |
| The five capability axes and evaluation order | So grants mean the same thing everywhere |
| Sense isolation rules | So an event never becomes an action silently |
| Availability flags | So agents plan the same way on every runtime |

Your runtime is an `HcpRuntimeDriver`:

```ts
import type { HcpRuntimeDriver, HcpAuthzContext } from '@harnesscontextprotocol/sdk'

export function createAcmeRuntime(deps: { catalog: Catalog; hub: Hub; rbac: Rbac; audit: Audit }): HcpRuntimeDriver {
  return {
    id: 'acme',
    kind: 'proprietary',

    listPacks: () => deps.catalog.list(),
    getPack:   (id) => deps.catalog.get(id),

    async connect(pack_id, authz) {
      if (!deps.rbac.has(authz, 'pack.connect', pack_id)) return { ok: false, error: 'denied: missing pack.connect' }
      return deps.hub.connect(pack_id, authz.principal)
    },

    async exec({ pack_id, verb_key, args = {}, authz, sandbox }) {
      const pack = await deps.catalog.get(pack_id)
      if (!pack) return { ok: false, pack_id, verb_key, error: `unknown pack ${pack_id}` }
      const policy = pack.policy.verbs[verb_key]
      if (!policy) return { ok: false, pack_id, verb_key, error: `no policy for ${verb_key}` }

      const cap = policy.operation === 'write' ? 'verb.exec.write' : 'verb.exec.read'
      if (!deps.rbac.has(authz, cap, pack_id, verb_key)) return { ok: false, pack_id, verb_key, error: `denied: missing ${cap} for ${pack_id}.${verb_key}` }
      if (policy.privacy.land === 'brain' && !authz.project_id) return { ok: false, pack_id, verb_key, error: 'denied: privacy.land brain requires project_id' }
      if (policy.confirm && args.confirm !== true && args.apply !== true) return { ok: false, pack_id, verb_key, error: 'confirm required' }

      const cred = await deps.hub.mint(pack, authz)          // fail closed inside
      const result = await runAdapter(pack, verb_key, args, cred, sandbox)
      if (policy.operation === 'write') await deps.audit.write({ authz, pack_id, verb_key, binding: cred.identity })
      return result
    },
  }
}
```

```ts
const hcp = await Hcp.create({ runtime: createAcmeRuntime(deps), grants: [] })
```

`createProprietaryStubRuntime` is an in-memory version of this shape, so you can
test multi-runtime behaviour before your real dependencies exist. A company
runtime meets the same [contract](#the-contract): a private catalog is fine, the
JSON face of the exec envelope is required and the CLI face only if you expose
one, and the five capability axes are the minimum.

Because packs never reference a runtime, the same `ledger` pack activates on the
reference-local runtime in CI, a developer's desktop in native-local, and a
company runtime in production, with the same Skill, Policy and commands. Only
the hub that mints and the RBAC that grants differ.

## Related

- [Implementations](/docs/community/implementations)
- [Specification: Conformance](/docs/specification/conformance)

## Sitemap

- [Documentation](https://harnesscontextprotocol.io/docs)
- [Packs](https://harnesscontextprotocol.io/packs)
- [Glossary](https://harnesscontextprotocol.io/docs/reference/glossary)
- [llms.txt](https://harnesscontextprotocol.io/llms.txt)
- [llms-full.txt](https://harnesscontextprotocol.io/llms-full.txt)
- [OpenAPI](https://harnesscontextprotocol.io/openapi.json)
- [MCP](https://harnesscontextprotocol.io/mcp)
