@brightbeamai/chap-coordinator
v0.3.4
Published
Collaborative Human-Agent Protocol (CHAP): reference TypeScript implementation. Coordinator with Core + every shipped profile, plus pluggable storage and typed method facade.
Maintainers
Readme
@brightbeamai/chap-coordinator (TypeScript)
TypeScript reference implementation of the Collaborative Human-Agent Protocol (CHAP).
Covers Core plus every profile, 39 method handlers in total. Spec-aligned
field names, error codes, and response shapes against profiles/*.md.
The Python reference at packages/coordinator-py/ implements the same
surface, so the two interoperate on the same JSON-RPC 2.0 wire.
Install
npm install @brightbeamai/chap-coordinatorNode 20+ required. No required runtime dependencies; uses Node's built-in
crypto for Ed25519 and JCS. better-sqlite3 is an optional dependency,
needed only for SqliteStore.
Companion packages
The @brightbeamai/chap-coordinator package is the protocol core. Two transport
adapters ship alongside it for embedding a Coordinator in MCP and
A2A ecosystems:
@brightbeamai/chap-coordinator-mcpwraps a Coordinator as an MCP server. Every CHAP method becomes an MCP tool namedchap.<method>. Reference stdio server atreference/mcp-server-ts/. Spec target: MCP 2026-07-28, serving 2025-11-25 clients as well.@brightbeamai/chap-coordinator-a2awraps a Coordinator as an A2A agent. Every CHAP method becomes anAgentSkillon the Agent Card. Reference Express server atreference/a2a-server-ts/. Spec target: A2A 0.3.0.
Inward citation helpers (wrapMcpToolCall, wrapA2aMessageExchange,
contentHash) are exported from @brightbeamai/chap-coordinator directly. They
take a completed external event and emit the matching CHAP audit
entries with input/output hashes.
Quick start
import { Coordinator } from "@brightbeamai/chap-coordinator";
const coord = new Coordinator({
defaultProfiles: ["core/1.0", "review/1.0", "whisper/1.0"],
});
coord.dispatch({
jsonrpc: "2.0", id: "1",
method: "workspace.create",
params: { workspace: "wsp_demo" },
});
coord.dispatch({
jsonrpc: "2.0", id: "2",
method: "participant.join",
params: { workspace: "wsp_demo",
from: "human:me@local", type: "human", role: "reviewer" },
});
coord.dispatch({
jsonrpc: "2.0", id: "3",
method: "participant.join",
params: { workspace: "wsp_demo",
from: "agent:bot", type: "agent", role: "drafter" },
});
const resp = coord.dispatch({
jsonrpc: "2.0", id: "4",
method: "task.create",
params: {
workspace: "wsp_demo",
from: "human:me@local",
kind: "draft_response",
input: { ticket_id: "INC-1" },
assignee: "agent:bot",
},
});
console.log(resp.result); // { task_id: "tsk_...", state: "created" }What is implemented
| Profile | Methods (per profile spec) |
|-----------------------|-------------------------------------------------------------------------------------------|
| core/1.0 | workspace.create, workspace.describe, workspace.set_profiles, participant.join, participant.leave, task.create, task.update, task.complete, audit.read; a task created with review_required completes only on a reviewer decision |
| review/1.0 | review.request, decide.approve, decide.reject, decide.override, abstain.declare, escalate.raise |
| whisper/1.0 | whisper.ask, whisper.answer; lapse hook via coord.checkWhisperLapses(workspaceId, now?) |
| deliberation/1.0 | deliberate.open, deliberate.comment, deliberate.vote, deliberate.close |
| handoff/1.0 | handoff.propose, handoff.accept, handoff.decline; multi-task; recipient may be a URI or group: |
| control/1.0 | control.pause, control.resume, control.cancel, control.snapshot, control.rollback, control.supersede, control.set_mode_ceiling; pause/resume take scope: task/participant/workspace |
| routing/1.0 | task.route, review.depth, escalate.auto; each emits a route_decision artefact |
| modes/1.0 | Mode handling at task.create; trial mode forces review_required; control.set_mode_ceiling shared with control/1.0 |
| security-signed/1.0 | participant.rotate_key, participant.revoke_key; top-level sig verified at dispatch when requireSignatures is true |
| audit-scitt/1.0 | audit.submit_to_scitt, audit.verify_receipt, audit.verify_chain; SCITT statements assembled and passed to a deployment-supplied submitter; local prev-hash chain linkage retained |
| identity-oidc/1.0 | participant.join binding via verifyOidcToken; cnf.jwk pinning, step-up auth via enforceStepUp |
| identity-vc/1.0 | participant.join binding via verifyVc; holder-key pinning |
39 method handlers in total, matching the Python reference exactly.
The review gate. A task carrying review_required does not complete on
task.complete. The call opens a review, holding the submitted output as the
artefact under review, and only a decide.* reaches completed. task.update
cannot complete such a task either, and answers -32602 naming the route
through. Where no reviewer was named, the set is the human members other than
the completer and the assignee; a workspace with no eligible human refuses the
completion rather than opening a review nobody can decide. Under modes/1.0 a
trial-mode task has review_required set for it.
Architecture
The coordinator is transport-agnostic, persistence-agnostic, and UI-agnostic. The entry point:
const response = coord.dispatch(envelope);envelope is a JSON-RPC 2.0 request; response is the JSON-RPC 2.0
response. State is held in memory; persist by subscribing with
coord.onAudit(listener).
Reference HTTP bindings:
reference/core/andreference/core-plus-review/(standalone TS servers used by the conformance harness and the demo playground)reference/python/(HTTP server using the Python reference)
Determinism for tests
const coord = new Coordinator({
deterministicIds: true,
deterministicClock: true,
});ULIDs derive from a deterministic counter; the clock advances by a fixed step per emission. Useful for golden-file tests.
Identity binding
const coord = new Coordinator({
verifyOidcToken: (token) => {
// Validate against your IdP; return claims dict or null.
// Include "cnf": { "jwk": {...} } to bind a signing key.
return { sub: "...", auth_time: 1234, cnf: { jwk: {...} } };
},
verifyVc: (vp) => {
// Verify the VP; return subject claims or null.
// Include "cnf_jwk": {...} to bind a holder key.
return { holder: "did:example:alice", cnf_jwk: {...} };
},
enforceStepUp: true, // require fresh auth_time for privileged methods
});If a participant.join envelope includes oidc_token or
vc_presentation, the relevant hook is called and the bound key is
added to the participant's key history.
Signed envelopes (security-signed/1.0)
const coord = new Coordinator({ requireSignatures: true });Each participant registers one or more JWKs at participant.join via
jwks: { keys: [...] }. Senders sign the JCS canonicalisation of the
envelope with the top-level sig field removed, then attach the
signature as a top-level sig: "ed25519:<kid>:<base64>" field. The
Coordinator looks up the verifying key by (from, kid, ts), so
historical envelopes verify across key rotation.
Key lifecycle:
participant.rotate_keyis signed with the old key; the Coordinator marksvalid_untilon the old key andvalid_fromon the new one.participant.revoke_keyis an admin operation; envelopes signed with the revoked key after revocation are rejected.
Helpers exported from @brightbeamai/chap-coordinator:
import {
deriveKeypair, // demo: deterministic keypair from a URI
signEnvelope, // sign canonical bytes -> ed25519:<kid>:<b64>
verifyEnvelope, // verify a sig tag against canonical + pubkey
publicKeyFromJwk, // JWK -> Node KeyObject
} from "@brightbeamai/chap-coordinator";Routing policies (routing/1.0)
A default policy ships out of the box. Override it with operator hooks:
const coord = new Coordinator({
routingPolicy: (task, candidates) => ({
selected: candidates[0],
rationale: {
policy_id: "round-robin",
summary: "first eligible",
},
}),
reviewDepthPolicy: (task, hints) => ({
depth: "spot_check",
sampling_probability: 0.05,
rationale: { policy_id: "v1", summary: "5% sample" },
}),
escalationPolicy: (task, hints) => ({
escalate: hints.criticality === "critical",
to: "group:senior-pool",
triggered_rule: { rule_id: "esc-crit", summary: "criticality=critical" },
}),
});Every call to task.route, review.depth, and escalate.auto
emits a route_decision artefact. task.route also updates the
task's assignee. The caller must be a workspace member. The default
policy takes the first candidate that is a member and is not paused; a
routingPolicy selection that is not a member is refused with
-32510, and one that is paused with -32063.
Audit and SCITT (audit-scitt/1.0)
Audit entries are hash-linked when the profile is active.
audit.verify_chain replays the chain locally and confirms
prev_hash continuity. The verdict names the range it covered:
status is verified with ok: true only when every entry was
checked, and not_evaluated with ok: false when part of the log
lies outside the chain, which happens when the profile is added to a
workspace that already has entries. A broken chain is an error. Read
status, or read ok and fail closed.
audit.submit_to_scitt builds a SCITT-shaped signed statement for
each audit entry in the requested range and passes it to a
deployment-supplied submitter:
const coord = new Coordinator({
scittSubmitter: (statement) => {
// Submit to your SCITT transparency service; return the receipt
return { receipt_id: "...", log_root: "..." };
},
});Without a submitter configured, audit.submit_to_scitt returns the
statements so the caller can submit out-of-band.
Testing
npm testThe suite covers Core, every profile, JCS and Ed25519 conformance vectors, signed-envelope verification, OIDC/VC binding, the inward wrap helpers, and an end-to-end composition test exercising every method handler in one workspace.
Spec fidelity
This implementation was reviewed against every profile spec under
profiles/ and aligned with the documented field names, error codes,
and response shapes. The full audit notes are in the parent repo's
CHANGELOG.md, with the 0.2.2 entry covering
the original TypeScript expansion and 0.2.3 / 0.2.4 covering the MCP
and A2A transport additions.
Licence
Apache 2.0. This package ships the licence in its own LICENSE file.
Specification
Tracks the spec at github.com/BrightbeamAI/chap/SPECIFICATION.md.
