Peer links
How an EvalBook project is linked to a CodeBook or MadeBook workspace, how every call between the products is authenticated, signed and made idempotent, and what the outbox and inbox guarantee.
A peer link records that an EvalBook project and a workspace in a sibling product are the same work. It is stored on both sides, and it is what authorizes everything that crosses between the products: nothing happens between an unlinked project and a workspace. EvalBook joins the peer protocol CodeBook and MadeBook already share; this page is EvalBook's side of it, and anything not written in the contract is not promised.
The directory comes from Bookbag SSO
Nothing about a peer is configured on EvalBook's server. EvalBook reads the peer directory from SSO's app registry with its own app token: each product's name and API base, EvalBook's own public API base, and the shared peer secret. EvalBook is the app evalbook with the peer prefix /project/api/peer (its work container is a project). The answer is cached for a minute, refreshed in the background, and the last good answer is kept through an SSO outage. A product switched off at SSO is an answer, not an outage: its peers disappear until it is switched on again. When the secret is rotated at SSO, the previous one is accepted for one hour.
For local work, PEER_SECRET, PEER_CODEBOOK_URL, PEER_MADEBOOK_URL and EVALBOOK_PUBLIC_API_URL override the directory; their names, never their values, are logged at boot. Production sets none. See Local development.
Every call
| Rule | Detail |
|---|---|
| Authentication | Authorization: Bearer <peer secret> and X-Peer-App: <caller slug>. |
| Signature | X-Peer-Signature: sha256=<hex HMAC-SHA256(peer secret, raw body)>. EvalBook requires it on every peer endpoint it serves and signs every call it makes. |
| Acting for a person | The call names organization_id and sub (or email); the receiver reads that person's role live from SSO. A call with no person is the product acting for itself, limited to what the link allows. |
| Idempotency | Writes carry Idempotency-Key. The receiver stores (caller, key, body digest); a replay answers the first answer; the same key with a different body is 409 IDEMPOTENCY_CONFLICT and nothing happens. |
| Refusals | Non-2xx with { data:false, code, error }. An event the receiver does not act on is acknowledged 2xx and recorded as ignored. |
| Callbacks | A callback URL must be under the caller's API base from the directory. |
| The link in every event | Every event body carries link: { evalbook_project_id, peer_workspace_id }. An event without it is answered 400. |
Making and ending a link
EvalBook serves the standard link endpoints under /project/api/peer/: workspaces (the projects the person administers), link, unlink and events. A project admin (link.manage) links a workspace under Integrations. Linking from EvalBook asks the peer first and records the link here only when the peer recorded its side; a link refused here afterwards is taken back there. One active link per project per product.
Ending a link here tells the peer through the outbox (link.unlinked), and revokes everything issued for the link. A link.unlinked from the peer ends the link here without an echo. A MadeBook link cannot be ended from EvalBook without MadeBook's allow on an unlink_governance decision; the page says so and points to MadeBook. CodeBook links end from either side. See MadeBook as a peer.
After any unlink, actions that were waiting on a MadeBook decision stay blocked; a person re-requests them under the standalone rule. Nothing is activated silently.
The outbox
Every event EvalBook owes a peer is written as an outbox row inside the transaction of the fact it announces, so a report that exists has its evaluation.completed row and a report that was rolled back has none. The delivery queue sends it signed and keyed, with backoff, for 17 attempts; after the last it is marked failed, shown under the organization's Deliveries page, and retryable by a person. Nothing is dropped. The outbox key is unique per target app, so one fact reaches CodeBook and MadeBook once each.
Events EvalBook emits, each with the link in its body:
| Event | Key | Body |
|---|---|---|
evaluation.completed, .failed, .cancelled |
<event>:<evaluation id> |
{ evaluation: { id, status, result, report_id, report_digest, subject_digest } } — the receiver re-fetches the report |
report.superseded |
report.superseded:<old>:<new> |
{ report_id, superseded_by } — the receiver re-fetches both |
suite.activated |
suite.activated:<suite version id> |
{ suite_id, version, digest } |
proposal.created, proposal.updated |
proposal.<x>:<id>:<revision> |
{ proposal: { id, state, … } } |
link.unlinked |
link.unlinked:PeerLink:<id> |
the link |
The inbox
Inbound events arrive at /project/api/peer/events with an Idempotency-Key. The inbox deduplicates by (source app, key): the same key with the same body digest is a duplicate and answers the first answer; a different digest is a recorded conflict (409) and has no second effect. Each component registers the handlers for the events it understands; the handler does database work only, inside the inbox transaction. Unknown events are acknowledged and recorded as ignored. Organization owners and admins can read the inbox and its conflicts under the organization's pages.
Requests, not events
Peer requests (evaluate, status, report, cancel, suites, traces, reports for a subject) are routes of the owning component under the same prefix, authenticated the same way. They are described with the peer that calls them: CodeBook as a peer and MadeBook as a peer.
What has run for real
Linking and unlinking both ways, signed delivery with a failure left visible and retryable, the inbox's deduplication, and the MadeBook decision flow have all been exercised in the application's end-to-end tests against stand-ins that sign and verify as the contract says. EvalBook has never talked to the real CodeBook or the real MadeBook. The sibling halves are deployed and were tested against an EvalBook stand-in. See The capability register.