The difficult part of connecting an agent to GitLab was deciding which event should start work, and where the state of that work should live. My first connector mirrored tasks and kept dispatch journals. That gave me two places to reconcile the same issue.
The current bridge is smaller: authenticate a GitLab event, resolve its explicitly addressed recipients, then create or reuse a native Paperclip session and send a notification. GitLab owns the issue, MR, review and pipeline. Here are the contracts behind that split.
The path from assignment to deployment
View the architecture at full size.
The bridge accepts added assignees, reviewer assignments, explicit re-review requests and direct, unquoted mentions of mapped agents. It suppresses self-notifications. Ordinary edits, pushes and pipeline events do not wake workers. Failed-pipeline routing belonged to the earlier connector; it is absent from this version.
The company configuration binds each allowed GitLab identity to one Paperclip agent. This shortened example uses invented IDs:
{
"companyId": "11111111-1111-4111-8111-111111111111",
"gitlabBaseUrl": "https://gitlab.example.com",
"gitlabSigningTokenRef": {
"type": "secret_ref",
"secretId": "22222222-2222-4222-8222-222222222222",
"version": "latest"
},
"projects": [{"id": 42, "path": "lab/example"}],
"agentMappings": [
{
"gitlabUserId": 7,
"gitlabUsername": "build-agent",
"paperclipAgentId": "33333333-3333-4333-8333-333333333333"
}
]
}
Both the numeric project ID and the path must match. The bridge validates the entire recipient set against the configured company before invoking anyone. These mappings belong in the existing managed configuration; installing the plugin should not invent accounts or provision credentials on demand.
Authenticate the bytes that GitLab signed
The receiver is /api/plugins/paperclip-gitlab-plugin/webhooks/gitlab-events. X-Paperclip-Company-Id selects the company's configuration and bound signing secret. That header alone establishes no authority.
With GitLab's signing-token mechanism, the verification input is:
webhook-id + "." + webhook-timestamp + "." + original request body
The bridge decodes the whsec_ key, checks a five-minute timestamp window and compares an HMAC-SHA256 digest against a v1, signature using timingSafeEqual. It also rejects malformed or ambiguous signing headers. Signature verification precedes event parsing and routing.
A JSON round-trip changes this contract. JSON.stringify(JSON.parse(rawBody)) may remove whitespace or alter formatting, so it is not a substitute for the signed bytes. The executable contract extract includes the full verification routine. Its tests cover a valid signature, a trailing newline added to the body, an expired timestamp, a wrong key and duplicate header names with different casing.
Signing authenticates a delivery. It does not grant the recipient permission to merge or deploy.
Give the work a stable identity
A new assignment and a later review comment should reach the same agent session when they refer to the same work item. The session key uses six fields:
import {createHash} from 'node:crypto';
export function sessionKey({companyId, origin, projectId, kind, iid, agentId}) {
const identity = [companyId, origin, projectId, kind, iid, agentId];
const digest = createHash('sha256').update(JSON.stringify(identity)).digest('hex');
return `plugin:paperclip-gitlab-plugin:session:gitlab:${digest}`;
}
Inputs are already validated at this point. The origin is normalized, and the object kind is canonicalized to issues or merge_requests. Issue and Work Item events referring to the same issue therefore share an identity. An issue and MR with the same IID do not. A second recipient gets a different session.
Delivery IDs, comment IDs and commit hashes are deliberately absent. The following commands exercise the downloadable extract with Node.js 22 or later:
curl --fail --output paperclip-contract.mjs \
https://www.cloudsium.de/examples/blog/paperclip-contract.mjs
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import {sessionKey} from './paperclip-contract.mjs';
const work = {
companyId: 'company-a', origin: 'https://gitlab.example.com',
projectId: 42, kind: 'issues', iid: 7, agentId: 'agent-a'
};
assert.equal(sessionKey(work), sessionKey({...work, deliveryId: 'retry'}));
assert.notEqual(sessionKey(work), sessionKey({...work, kind: 'merge_requests'}));
console.log('Session identity checks passed');
JS
The host must actually support idempotent session creation for this key. A stable string passed to a create-only API is insufficient. Reusing a session also does not deduplicate messages: repeated valid deliveries can append another notification. The transport is at least once, so the worker must reread current GitLab state before acting.
Keep execution authority visible
The notification contains bounded metadata and a link built from the configured origin. The agent reads the current issue or MR through its own authorized GitLab connection. The bridge neither carries a general-purpose GitLab token nor writes GitLab objects itself.
| Decision | Owner | Evidence to inspect |
|---|---|---|
| Who was addressed? | Bridge configuration | Native assignment or unquoted mention, mapped recipient |
| May the agent run? | Paperclip host | Company membership and current agent status |
| What may change? | GitLab permissions and task scope | Current issue, repository and MR diff |
| Is this revision approved? | Reviewer and human release policy | Review of the current MR head |
| What gets deployed? | Owning repository's CI | Successful pipeline for the approved revision |
Paused, terminated and pending-approval agents are suppressed. The bridge never resumes them and has no replay queue for suppressed events. Resuming an agent does not recover a missed assignment automatically.
My rollout currently admits one selected worker on demand. The wider implementation → independent review → human approval → owning pipeline sequence is the delivery contract being qualified, not a claim that an autonomous team has completed every stage in production. GitLab's approval enforcement must also be checked against the installed tier and settings.
The useful acceptance test is one traceable issue: deliver an assignment, observe the native session, repeat the event, pause the recipient, and confirm that deployment still requires the approved MR and its pipeline. That exercises the boundaries a “plugin loaded” message cannot prove.