Duo Messaging: WorkspaceProjectService for duo-workspace project
Problem
The Duo Messaging Service enables users to interact with Duo from external messaging services (Slack first, then Teams, etc.). Users @mention Duo, give it a task, and Duo works on it asynchronously using the existing Flows API (CI pipeline) infrastructure.
A key challenge is that CI pipelines require a project, but messaging services have no project context. This MR provides that foundation: a per-namespace duo-workspace project that serves as the default execution environment.
Where this fits
This is the PoC MR broken into reviewable pieces:
| # | Issue | MR | Description |
|---|---|---|---|
| 1 | #597569 (closed) | This MR | Workspace project service |
| 2 | #597570 (closed) | — | Orchestrator, adapter base, callback infrastructure |
| 3 | #597571 (closed) | — | Slack adapter and AppMentionedService wiring |
| 4 | #597573 | — | User-facing documentation |
What does this MR do?
Implements Ai::Messaging::WorkspaceProjectService, a find-or-create service that provides a duo-workspace project per namespace:
- Finds an existing project by path, or creates a new private project in the namespace
- Initializes with a README linking to AGENTS.md and agent-config.yml customization docs
- Disables unnecessary features (wiki, snippets, container registry, packages)
- Enables CI/CD builds (required for flow execution)
- Handles concurrent creation via
ActiveRecord::RecordNotUniquerescue + retry - Follows the same auto-creation pattern as Security Policy Projects
No feature flag needed — this service is inert without a caller.
Design decisions
Service account: reuse the developer/v1 catalog SA
During implementation, we investigated three approaches for the messaging service account:
| Option | Approach | Tradeoff |
|---|---|---|
A: Reuse developer/v1 catalog SA |
Messaging uses the existing SA created when admin enables the Developer flow | Messaging triggers developer/v1, so it should use its SA |
B: New messaging/v1 foundational flow |
New catalog entry with its own SA | No actual messaging/v1 workflow definition exists — bends the abstraction |
| C: Self-bootstrapping SA (PoC approach) | Messaging creates its own SA on-demand | Redundant — creates a second SA for the same flow in the same namespace |
We chose Option A. Messaging is a trigger mechanism for developer/v1, not a separate flow. The SA identity should reflect the flow being executed, not the trigger source. This means:
- No separate
ResolveServiceAccountServiceneeded (removed from original issue scope) - The
TriggerFlowService(MR #2 (closed)) will useAi::Catalog::ItemConsumers::ResolveServiceAccountServiceto resolve the existingdeveloper/v1SA - If the Developer flow is not enabled for the namespace, messaging returns a clear error guiding the user
Workspace project creation: tied to flow enablement
The workspace project will be created as part of enabling the Developer flow for a namespace (in ItemConsumers::CreateService), using the admin's permissions. This avoids permission issues at trigger time since regular users may not have create_projects access.
Follow-up needed: backfill for existing enablements
Namespaces that already have developer/v1 enabled before this ships won't have a workspace project. A follow-up is needed — likely a migration that creates the project for existing group-level developer/v1 item consumers.
How to test locally (GDK)
Setup
# In Rails console (bin/rails c)
user = User.find_by(username: 'root')Scenario 1: Fresh creation (happy path)
group = user.owned_groups.first
# Delete existing workspace project if present
existing = group.projects.find_by_path('duo-workspace')
::Projects::DestroyService.new(existing, user).execute if existing
service = Ai::Messaging::WorkspaceProjectService.new(namespace: group, current_user: user)
result = service.executeExpected:
result.success?→true- Project created at
<group>/duo-workspace - Visibility is
private - Wiki, snippets, container registry, packages disabled
- CI/CD builds enabled
Scenario 2: Idempotency (project already exists)
# Call the service again on the same group (from scenario 1)
result2 = service.executeExpected:
result2.success?→trueresult2.payload[:project].id == result.payload[:project].id→true(same project, no new creation)
Scenario 3: Permission denied (non-member, no existing project)
# Find a group with no existing duo-workspace
clean_group = user.owned_groups.detect { |g| g.projects.find_by_path('duo-workspace').nil? }
non_member = User.where.not(id: user.id).where(user_type: :human).first
clean_group.member?(non_member) # => false
result3 = Ai::Messaging::WorkspaceProjectService.new(
namespace: clean_group, current_user: non_member
).executeExpected:
result3.success?→falseresult3.messageincludes "permission"
Scenario 4: Existing project accessible regardless of membership
# Use a group where duo-workspace already exists (from scenario 1)
result4 = Ai::Messaging::WorkspaceProjectService.new(
namespace: group, current_user: non_member
).executeExpected:
result4.success?→true- Returns the existing project (find succeeds, no creation needed)
- Note: authorization for what the user can do with the project is the caller's responsibility
Scenario 5: README content
project = result.payload[:project]
readme = project.repository.blob_at(project.default_branch, 'README.md')
puts readme.dataExpected:
- Title:
# Duo Workspace - Mentions "default execution environment" and "not triggered from a specific project"
- Links to AGENTS.md docs: https://docs.gitlab.com/user/duo_agent_platform/customize/agents_md/
- Links to agent-config.yml docs: https://docs.gitlab.com/user/duo_agent_platform/flows/execution/
- Links to Duo Agent Platform docs: https://docs.gitlab.com/user/duo_agent_platform/
Scenario 6: Project settings
project = result.payload[:project]
project.visibility # => "private"
project.builds_enabled? # => true
project.wiki_enabled? # => false
project.snippets_enabled? # => false
project.container_registry_enabled # => false
project.packages_enabled # => false
project.description # => "Duo Agent workspace — customizable agent environment..."Related
- Issue: #597569 (closed)
- Parent: #590434
- PoC: !231853 (closed)
- ADR: gitlab-com/content-sites/handbook!19020 (merged)