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 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:
@harnesscontextprotocol:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}bun add @harnesscontextprotocol/runtime
bun add @harnesscontextprotocol/sdk
bun add -g @harnesscontextprotocol/cliA 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#
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#
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#
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.
| # | Owns | Fail-closed rule | Page |
|---|---|---|---|
| 1 | Load surface.json, policy.json and HARNESS.md from a pack directory | Reject on parse error | 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 |
| 3 | Authorise each exec against five capability axes for a Principal | Deny with a reason; never default-allow | Authorization |
| 4 | Gate confirm: true writes | Refuse before any adapter runs | Execution |
| 5 | Mint Binding through a hub for user, local and mothership | No ambient env fallback | Binding |
| 6 | Execute the Adapter: spawn the wrap bin with a rendered argv, or call in-process | Unknown bin denied | Execution |
| 7 | Sandbox by default: bin allow-list, cwd jail, no inherited credentials | native-local only by explicit opt-in | Sandbox |
| 8 | Audit writes | Full attribution tuple | Audit |
| 9 | Report availability per command: installed, activated, connected, executable | Every false carries a reason | Lifecycle |
| 10 | Route Sense (optional): accept declared triggers and record them in a sink | sense.enable off → drop | 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
mothershipBinding 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:
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
},
}
}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.