Skip to content

Commit 130d954

Browse files
committed
docs: add ADR 0043 for GitLab support via webhook bridge
Add ADR 0043 and companion implementation document for GitLab support. Architecture: a webhook bridge Cloud Function translates GitLab webhook payloads into pipeline trigger API calls, running dispatch on the protected main branch. Three defense-in-depth layers prevent unauthorized execution: hardcoded ref=main, protected CI/CD variables, and per-project webhook secret validation. Key design decisions: - Forge-neutral interface methods (CreateRoleCredential/RevokeRoleCredential) - Token mint extended with GitLab OIDC claim normalization - Per-project PATs stored in Secret Manager with project-scoped naming - Pull-based alternative documented for self-hosted instances behind VPNs - Event payload passed via temp file to avoid ps aux exposure Supersedes ADR 0028 (deprecated). Signed-off-by: Greg Allen <gallen@redhat.com>
1 parent 1088f9b commit 130d954

10 files changed

Lines changed: 1214 additions & 10 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ This is not a product spec. It's an evolving exploration of a hard problem space
3333
- [Performance Verification](docs/problems/performance-verification.md) — Catching agent-introduced performance regressions before they reach production
3434
- [Production Feedback](docs/problems/production-feedback.md) — How platform execution signals feed back into what agents work on and how they assess risk
3535
- [Testing the Agents](docs/problems/testing-agents.md) — CI for prompts: regression testing, eval frameworks, and behavioral verification for agent instructions
36-
- [GitLab Implementation](docs/problems/gitlab-implementation.md) — Implementation details for GitLab support: webhook security, dispatch pipelines, forge interface evolution
36+
- [GitLab Support](docs/problems/gitlab-support.md) — Webhook bridge architecture, token mint extension, credential model, and phased rollout plan for GitLab support
37+
- [GitLab Implementation](docs/problems/gitlab-implementation.md)*(Superseded by [gitlab-support.md](docs/problems/gitlab-support.md))* Original implementation details for the ADR-0028 approach
3738
- [Operational Observability](docs/problems/operational-observability.md) — How do the humans operating an autonomous software factory understand what it is doing, debug it when it goes wrong, and improve it over time?
3839
- [Adaptive Agent Selection](docs/problems/adaptive-agent-selection.md) — Learning which agent/team/workflow configurations work best for which problem classes, using evolutionary algorithms and Thompson Sampling
3940
- [Platform Nativeness](docs/problems/platform-nativeness.md) — When the platform you automate is also the one you build on: which problems are inherent vs. self-inflicted

docs/ADRs/0028-gitlab-support.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "28. GitLab Support Architecture"
3-
status: Deprecated
3+
status: Superseded
44
relates_to:
55
- agent-infrastructure
66
- agent-architecture
@@ -17,7 +17,7 @@ Date: 2026-04-29
1717

1818
## Status
1919

20-
Deprecated
20+
Superseded by [ADR 0043](0043-gitlab-support-via-webhook-bridge.md).
2121

2222
## Context
2323

docs/ADRs/0031-reusable-workflows-for-action-installed-distribution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ caller's perspective.
141141
inline, which partially mitigates this.
142142
- **GitHub-specific mechanism:** `workflow_call` and `secrets:` passthrough are
143143
GitHub Actions primitives with no direct equivalent in other CI systems.
144-
Multi-forge support ([ADR 0028](0028-gitlab-support.md)) will need its own
144+
Multi-forge support ([ADR 0043](0043-gitlab-support-via-webhook-bridge.md), supersedes ADR 0028) will need its own
145145
distribution mechanism (e.g., GitLab CI/CD Components or `include:`)
146146
independent of this ADR.
147147
- **Scaffold output changes:** `fullsend admin install` will emit thin callers

docs/ADRs/0036-agent-execution-sandbox.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Accepted
2121

2222
## Context
2323

24-
Fullsend agents execute within isolated sandboxes that enforce security boundaries: filesystem access control, network policy enforcement, and credential isolation (ADR-0017, ADR-0025). The current implementation uses OpenShell with per-agent L7 network policies and runs on GitHub Actions runners. With GitLab support proposed (ADR-0028), the execution architecture needs to work on both GitHub Actions and GitLab CI runners.
24+
Fullsend agents execute within isolated sandboxes that enforce security boundaries: filesystem access control, network policy enforcement, and credential isolation (ADR-0017, ADR-0025). The current implementation uses OpenShell with per-agent L7 network policies and runs on GitHub Actions runners. With GitLab support planned (ADR-0028, superseded by [ADR-0043](0043-gitlab-support-via-webhook-bridge.md)), the execution architecture needs to work on both GitHub Actions and GitLab CI runners.
2525

2626
The sandbox architecture has multiple concerns that need to be resolved together:
2727

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
---
2+
title: "43. GitLab support via webhook bridge"
3+
status: Accepted
4+
relates_to:
5+
- agent-infrastructure
6+
- agent-architecture
7+
- gitlab-support
8+
topics:
9+
- gitlab
10+
- forge
11+
- ci-cd
12+
- multi-platform
13+
- webhook
14+
- security
15+
---
16+
17+
# 43. GitLab support via webhook bridge
18+
19+
Date: 2026-06-01
20+
21+
## Status
22+
23+
Accepted
24+
25+
Supersedes [ADR 0028](0028-gitlab-support.md) (Deprecated).
26+
27+
## Context
28+
29+
fullsend supports GitHub exclusively. Organizations on GitLab cannot adopt it. The forge abstraction (`forge.Client`, [ADR 0005](0005-forge-abstraction-layer.md)) was designed for multi-forge support, but the surrounding infrastructure — token mint, dispatch workflows, shim security model — is GitHub-specific.
30+
31+
[ADR 0028](0028-gitlab-support.md) proposed GitLab support in April 2026 but was deprecated because the architecture changed significantly:
32+
33+
- **ADR 0029** introduced the central token mint (GCP Cloud Function with OIDC validation), replacing PEM-in-repo-secrets.
34+
- **ADR 0041** moved dispatch from asynchronous `workflow_dispatch` to synchronous `workflow_call`.
35+
- **ADR 0031** introduced reusable workflows so `.fullsend` agent workflows are thin callers to upstream `fullsend-ai/fullsend`.
36+
- **ADR 0033** added per-repo installation mode alongside per-org.
37+
38+
ADR 0028's authentication model (per-role Project Access Tokens stored as CI/CD variables), dispatch model (async pipeline trigger API), and unresolved webhook translation question no longer align with the current architecture. This ADR redesigns GitLab support from the current baseline.
39+
40+
### The core problem: no `pull_request_target`
41+
42+
On GitHub, `pull_request_target` runs the shim workflow from the **base branch**, preventing MR authors from modifying the workflow to exfiltrate secrets ([ADR 0009](0009-pull-request-target-in-shim-workflows.md)). GitLab has no equivalent mechanism. A `.gitlab-ci.yml` in an enrolled repo runs the MR branch version — an attacker could modify it to dump secrets.
43+
44+
This is the fundamental security gap that any GitLab support design must close.
45+
46+
### Webhook-to-pipeline wire incompatibility
47+
48+
GitLab CI/CD pipelines cannot be natively triggered by issue events, note (comment) events, or MR review events. The `CI_PIPELINE_SOURCE` variable has no values for these event types.
49+
50+
GitLab webhooks _can_ fire on these events, but webhooks deliver JSON payloads while the pipeline trigger API (`/api/v4/projects/:id/trigger/pipeline`) expects form-encoded parameters. Pointing a webhook URL at the trigger API produces a malformed request. An intermediary is required.
51+
52+
## Options
53+
54+
**Selected**: Webhook bridge Cloud Function (see [Decision](#decision) below).
55+
56+
### Alternative 1: In-repo CI job as shim
57+
58+
A `.gitlab-ci.yml` job in enrolled repos receives pipeline events and calls the `.fullsend` trigger API.
59+
60+
**Rejected**: Cannot enforce protected-branch-only execution. MR authors can modify the CI job to exfiltrate the trigger token or any secrets available in the pipeline context. This reintroduces the exact vulnerability that `pull_request_target` prevents on GitHub.
61+
62+
### Alternative 2: GitLab serverless functions
63+
64+
Deploy a GitLab-hosted serverless function that translates webhooks to trigger API calls.
65+
66+
**Rejected**: Requires GitLab Premium or Ultimate tier. Excluding Free tier users is an unacceptable constraint for an open-source project.
67+
68+
### Alternative 3: Per-role Project Access Tokens stored as CI/CD variables
69+
70+
Store PATs directly in `.fullsend` CI/CD variables (ADR 0028's original approach) instead of using the central token mint.
71+
72+
**Rejected**: Diverges from the token mint model (ADR 0029). Managing credentials in two different systems (Secret Manager for GitHub, CI/CD variables for GitLab) increases operational complexity and the attack surface. The mint model is superior because it centralizes credential storage, enforces OIDC validation, and scopes tokens per-request.
73+
74+
### Alternative 4: Pull-based model with GitLab CI scheduled pipelines
75+
76+
Use GitLab CI jobs for events that CI can natively trigger (MR events via `merge_request_event` pipeline source). For everything else (issue events, note events, label changes), use a scheduled GitLab CI job that polls the GitLab API for recent activity and dispatches agent work.
77+
78+
**Advantages**: No external infrastructure needed — everything runs in GitLab CI. Scheduled jobs run in the context of the target repo and have access to repo secrets. No webhook bridge to deploy, monitor, or make network-adjacent to self-hosted instances.
79+
80+
**Deferred for further evaluation**: The pull model trades latency for simplicity — scheduled pipelines have a minimum 5-minute interval, so issue triage could be delayed up to 5 minutes. It also requires idempotent event processing (tracking which events have already been handled across polling intervals). However, for organizations where the bridge's per-instance deployment cost is prohibitive (especially self-hosted GitLab behind VPNs), this may be the better trade-off. This alternative warrants a proof-of-concept alongside the webhook bridge.
81+
82+
## Decision
83+
84+
### Webhook bridge Cloud Function
85+
86+
Deploy a lightweight GCP Cloud Function — separate from the token mint — that translates GitLab webhook payloads into pipeline trigger API calls.
87+
88+
```
89+
GitLab webhook (JSON)
90+
→ Bridge Cloud Function
91+
→ validates webhook secret (constant-time)
92+
→ extracts event type and payload
93+
→ base64-encodes payload (prevents YAML injection)
94+
→ calls Pipeline Trigger API with ref=main (hardcoded)
95+
→ .fullsend dispatch pipeline (protected main branch)
96+
→ determines stage from event payload
97+
→ triggers child pipeline via trigger: include: artifact:
98+
→ child pipeline authenticates to token mint via GitLab OIDC
99+
→ agent executes in sandbox
100+
```
101+
102+
**Why a separate function, not co-deployed with the mint**: The bridge handles untrusted webhook payloads from the internet. If it has a vulnerability, compromise of a separate function gives the attacker only a pipeline trigger token. Co-deployed, a bridge exploit could reach the mint's Secret Manager access and all stored PEM keys.
103+
104+
**Infrastructure cost**: Unlike the token mint (which can be centralized), the bridge must be network-reachable from the GitLab instance that sends webhooks. For GitLab.com this is straightforward (public endpoint). For self-hosted GitLab instances behind VPNs, the bridge must be deployed adjacent to each instance — potentially requiring a separate bridge deployment per GitLab instance with distinct network configurations. This is a meaningful operational cost beyond "one more Cloud Function." Each bridge deployment needs its own monitoring, trigger tokens, and webhook token cache. Organizations with multiple self-hosted GitLab instances should weigh this against the pull-based alternative (Alternative 4).
105+
106+
**Why this is acceptable now**: [ADR 0009](0009-pull-request-target-in-shim-workflows.md) rejected hosted webhook receivers for "breaking compute-platform agnosticism." ADR 0029 subsequently introduced a hosted GCP Cloud Function (the token mint). Bridge functions follow the same deployment pattern. The per-instance cost is real but bounded — most organizations have one or two GitLab instances, not dozens.
107+
108+
### Defense in depth
109+
110+
Three independent layers prevent unauthorized pipeline execution:
111+
112+
1. **Bridge hardcodes `ref=main`**: The target ref is never derived from the webhook payload. Even if an attacker crafts a malicious webhook, the dispatch pipeline always runs on the protected default branch.
113+
114+
2. **Protected CI/CD variables**: All secrets in the `.fullsend` project (trigger tokens, role credentials, webhook tokens) are marked as "protected." GitLab only exposes protected variables to pipelines running on protected branches. If the bridge is compromised to call the trigger API with `ref=attacker-branch`, secrets are not exposed.
115+
116+
3. **Per-project webhook secret validation**: Each enrolled project has a unique webhook secret token, stored as a protected CI/CD variable in `.fullsend` (variable name: `WEBHOOK_TOKEN_<sha256(project_path)>` for collision-free identification). The dispatch pipeline validates the token before processing.
117+
118+
### Token mint extension
119+
120+
The existing token mint ([ADR 0029](0029-central-token-mint-secretless-fullsend.md)) is extended with a credential backend abstraction:
121+
122+
- **`GitHubCredentialBackend`** (existing): OIDC → validate claims → retrieve PEM from Secret Manager → generate JWT → exchange for scoped GitHub App installation token.
123+
- **`GitLabCredentialBackend`** (new): OIDC → validate claims → retrieve stored Project Access Token from Secret Manager → return token.
124+
125+
GitLab OIDC integration:
126+
127+
- GitLab CI jobs declare `id_tokens:` with audience `fullsend-mint`.
128+
- A WIF provider for the GitLab OIDC issuer is added to the existing WIF pool.
129+
- GitLab claim names are normalized to the mint's internal representation: `project_path``repository`, `namespace_path``repository_owner`.
130+
- The mint validates the pipeline originated from the `.fullsend` project (or a registered per-repo project) on a protected ref.
131+
132+
### Credential model
133+
134+
GitLab Project Access Tokens (PATs) replace GitHub Apps:
135+
136+
- Created per-role per-project during `fullsend admin install` via GitLab API.
137+
- Stored in Secret Manager with project-scoped naming: `fullsend-{group}--{project}--{role}-pat`. Including the project name is necessary because PATs are per-project (unlike GitHub App PEMs which are per-org).
138+
- Role mapping: triage → Reporter, code → Developer, review → Developer, fix → Developer, fullsend (orchestrator) → Maintainer.
139+
- Max 1-year expiry. `fullsend admin analyze` warns when PATs are within 30 days of expiry. `fullsend admin rotate-tokens` performs rotation.
140+
141+
**Mint statefulness trade-off**: Unlike GitHub PEMs (one per org per role), GitLab PATs are per-project per role. This means the mint must store credentials that scale with the number of enrolled projects, making it less stateless than the GitHub model. For organizations where this scaling is a concern, storing PATs as protected CI/CD variables in the `.fullsend` project (bypassing the mint entirely) is a viable alternative — see Alternative 3 and the open question below.
142+
143+
**PAT limitation**: GitLab PATs cannot be used to mint further scoped-down tokens — they are the final credential. The mint returns the PAT directly rather than exchanging it for a short-lived token. This means PAT scope must be carefully set at creation time (per-role mapping above). This is a security regression compared to GitHub's model where the mint generates short-lived, repo-scoped installation tokens from long-lived PEMs.
144+
145+
### GitLab dispatch pipeline
146+
147+
The `.fullsend` project's dispatch pipeline mirrors GitHub's `dispatch.yml`:
148+
149+
- Triggered by the bridge function via Pipeline Trigger API.
150+
- Validates the source project is enrolled (config.yaml lookup).
151+
- Determines the stage from the base64-decoded event payload (same routing logic as GitHub: issue events, note events, MR events, label events).
152+
- Generates a child pipeline config that includes the matching stage file.
153+
- Triggers the child pipeline via `trigger: include: artifact:` with `strategy: depend` (synchronous, preserving the run-correlation property from [ADR 0041](0041-synchronous-workflow-call-event-dispatch.md)).
154+
155+
### Forge abstraction evolution
156+
157+
New methods added to `forge.Client`:
158+
159+
```go
160+
CreateWebhook(ctx, owner, repo, targetURL, secretToken string, events []string) (webhookID string, err error)
161+
DeleteWebhook(ctx, owner, repo, webhookID string) error
162+
CreateRoleCredential(ctx, owner, repo, roleName string, scopes []string, expiresAt time.Time) (credential string, credentialID string, err error)
163+
RevokeRoleCredential(ctx, owner, repo, credentialID string) error
164+
IsProtectedBranch(ctx, owner, repo, branch string) (bool, error)
165+
```
166+
167+
`CreateRoleCredential`/`RevokeRoleCredential` abstract over both GitHub Apps and GitLab PATs. The GitHub implementation returns `forge.ErrNotSupported` since GitHub credentials are managed through the token mint's PEM-based flow, not through the forge interface.
168+
169+
GitHub-specific methods (`ListOrgInstallations`, `GetAppClientID`) move to an extension interface (`GitHubExtensions`). Callers that need them type-assert.
170+
171+
### Installation modes
172+
173+
Both per-org and per-repo modes are supported for GitLab:
174+
175+
- **Per-org**: `.fullsend` is a project within the GitLab group. Enrolled projects configure webhooks pointing to the bridge. The dispatch pipeline and agent pipelines run in `.fullsend`.
176+
- **Per-repo**: `.fullsend/` directory lives within the target project. The webhook points to the bridge, which triggers the project's own pipeline on the protected default branch.
177+
178+
### Platform requirements
179+
180+
- **Minimum GitLab version**: 17.0+ (OIDC `id_tokens:` support, CI/CD components, current API stability).
181+
- **Self-hosted support**: Configurable instance URLs via `--gitlab-url` flag and `gitlab_instance_url` in config.yaml. The WIF pool needs per-instance OIDC providers.
182+
183+
## Consequences
184+
185+
### Positive
186+
187+
- Organizations on GitLab (both GitLab.com and self-hosted 17.0+) can adopt fullsend.
188+
- Reuses the central token mint model (ADR 0029) rather than introducing a parallel credential system.
189+
- The same agent workflow (triage → code → review → fix → retro) works identically from the user's perspective.
190+
- Webhook-based dispatch is architecturally cleaner than an in-repo shim for GitLab's security model.
191+
- Forge abstraction (ADR 0005) is validated and strengthened by a second implementation.
192+
193+
### Negative
194+
195+
- Two CI/CD template sets to maintain (`.github/workflows/` and `.gitlab/ci/`).
196+
- Project Access Tokens are less granular than GitHub Apps (role-level, not per-permission).
197+
- PATs require rotation (max 1-year expiry); GitHub App private keys do not expire.
198+
- The webhook bridge adds a Cloud Function to deploy and monitor.
199+
- Post-scripts may need minor forge-specific branches where GitHub and GitLab APIs diverge (e.g., review semantics).
200+
201+
### Risks
202+
203+
- **Protected branch misconfiguration**: If `.fullsend` project's default branch is not protected, MR authors could modify pipeline code. Mitigated by validation during install and protected CI/CD variables as defense-in-depth.
204+
- **Bridge function availability**: If the bridge is down, no GitLab events are processed. Mitigated by Cloud Function auto-scaling and health monitoring.
205+
- **GitLab API rate limits**: GitLab.com has lower rate limits than GitHub. Mitigated by exponential backoff and retry in the GitLab forge client.
206+
- **Self-hosted GitLab version drift**: Wide version range among self-hosted instances. Mitigated by requiring 17.0+ and detecting version during install.
207+
208+
## References
209+
210+
- [ADR 0005](0005-forge-abstraction-layer.md): Forge abstraction layer
211+
- [ADR 0007](0007-per-role-github-apps.md): Per-role GitHub Apps (authentication model to replicate)
212+
- [ADR 0009](0009-pull-request-target-in-shim-workflows.md): `pull_request_target` security model (problem to solve)
213+
- [ADR 0028](0028-gitlab-support.md): GitLab Support Architecture (deprecated, superseded by this ADR)
214+
- [ADR 0029](0029-central-token-mint-secretless-fullsend.md): Central token mint
215+
- [ADR 0031](0031-reusable-workflows-for-action-installed-distribution.md): Reusable workflows
216+
- [ADR 0033](0033-per-repo-installation-mode.md): Per-repo installation mode
217+
- [ADR 0041](0041-synchronous-workflow-call-event-dispatch.md): Synchronous dispatch
218+
- [GitLab support implementation details](../problems/gitlab-support.md): Companion implementation document

0 commit comments

Comments
 (0)