.md

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.

On this page

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 and prove it with the conformance vectors.

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.

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:

.npmrc
@harnesscontextprotocol:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
Terminal
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.

Create a runtime over pack directories#

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).

Activate, connect, exec#

TypeScript
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.

Report availability#

TypeScript
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 before it plans, so it does not discover a denial mid-run.

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, and any step may deny.

#OwnsFail-closed rulePage
1Load surface.json, policy.json and HARNESS.md from a pack directoryReject on parse errorExecution
2Lint on install: Policy covers every command and trigger, Skill present, wrap has named commands or pass_throughReject on any issueTesting
3Authorise each exec against five capability axes for a PrincipalDeny with a reason; never default-allowAuthorization
4Gate confirm: true writesRefuse before any adapter runsExecution
5Mint Binding through a hub for user, local and mothershipNo ambient env fallbackBinding
6Execute the Adapter: spawn the wrap bin with a rendered argv, or call in-processUnknown bin deniedExecution
7Sandbox by default: bin allow-list, cwd jail, no inherited credentialsnative-local only by explicit opt-inSandbox
8Audit writesFull attribution tupleAudit
9Report availability per command: installed, activated, connected, executableEvery false carries a reasonLifecycle
10Route Sense (optional): accept declared triggers and record them in a sinksense.enable off → dropSense 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:

DriverPurpose
reference-localFile-backed, in-process, sandbox allow-list, default authoriser. The conformance target
proprietary-stubIn-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 privateFreedom
Pack catalogPrivate, curated, versioned however you like: SQL tables, a directory, a private registry
Binding hubYour keyring, OAuth flows and refresh schedule, a local file store, an OS keychain
RBACYour role tables, row-level security, IdP and tenancy model, mapped onto the five capabilities
Sandbox depthProcess allow-list up to containers and microVMs
Sense sinkAn inbox, a queue, a webhook fan-out, UI badges
Principal mappingSession → { id, kind, tenant, project }
TransportIn-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 protocolWhy
The three files and their schemasSo packs are portable
Command twin face and verb_keySo invokers are interchangeable
Policy tokens and the confirm gateSo 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 orderSo grants mean the same thing everywhere
Sense isolation rulesSo an event never becomes an action silently
Availability flagsSo agents plan the same way on every runtime

Your runtime is an HcpRuntimeDriver:

TypeScript
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
    },
  }
}
TypeScript
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: 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.