Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

/**
* Migration to add git_repository_setup_mode column to agents table.
* Supports empty (git init) vs clone workspace initialization.
*/
export class AddGitRepositorySetupModeToAgentsTable1779800000000 implements MigrationInterface {
name = 'AddGitRepositorySetupModeToAgentsTable1779800000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'agents',
new TableColumn({
name: 'git_repository_setup_mode',
type: 'varchar',
length: '16',
isNullable: true,
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('agents', 'git_repository_setup_mode');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ The provisioning endpoint accepts a `ProvisionServerDto` with the following fiel
- `keycloakRealm` - Keycloak realm (optional, defaults to environment variable)
- `keycloakAuthServerUrl` - Keycloak auth server URL (optional, defaults to environment variable)
- `agentWsPort` - Agent WebSocket port (defaults to 8080)
- `gitRepositoryUrl` - Git repository URL for agent workspace
- `gitRepositorySetupMode` - `clone` (default) or `empty` (git init without remote)
- `gitRepositoryUrl` - Git repository URL for agent workspace (clone mode)
- `gitUsername` - Git username for repository access
- `gitToken` - Git token/personal access token for repository access
- `gitPassword` - Git password for repository access (alternative to token)
Expand Down Expand Up @@ -341,7 +342,8 @@ Values are securely passed from the frontend through the backend to the user dat

Client responses include a `config` field that is automatically fetched from the remote agent-manager service. This configuration includes:

- `gitRepositoryUrl` - The Git repository URL configured on the agent-manager instance (if set)
- `gitRepositoryUrl` - The Git repository URL configured on the agent-manager instance (if set, clone mode)
- `gitRepositorySetupMode` - Default setup mode for new agents (`clone` or `empty`)
- `agentTypes` - Array of available agent provider types registered on the agent-manager instance (e.g., `['cursor']`, `['cursor', 'opencode']`)

The config field is optional and may be `undefined` if:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4148,6 +4148,10 @@ components:
type: string
description:
type: string
gitRepositorySetupMode:
type: string
enum: [clone, empty]
description: How to initialize the agent workspace Git repository
gitRepositoryUrl:
type: [string, 'null']
description: Git repository URL to clone when creating the agent
Expand Down Expand Up @@ -4187,8 +4191,12 @@ components:
properties:
repositoryUrl:
type: string
description: Git repository URL associated with the agent
description: Git information (only present if gitRepositoryUrl is set)
description: Git repository URL associated with the agent (clone mode only)
setupMode:
type: string
enum: [clone, empty]
description: How the workspace repository was initialized
description: Git information when the agent has an associated repository or empty local repo
createdAt:
type: string
format: date-time
Expand Down Expand Up @@ -4220,6 +4228,10 @@ components:
gitRepositoryUrl:
type: [string, 'null']
description: Git repository URL used for cloning repositories when creating agents
gitRepositorySetupMode:
type: string
enum: [clone, empty]
description: Default setup mode for new agents on this instance
agentTypes:
type: array
items:
Expand Down Expand Up @@ -4540,9 +4552,15 @@ components:
minimum: 1
maximum: 65535
description: Optional Socket.IO port for the client's agents gateway (defaults to 8080 if not set)
gitRepositorySetupMode:
type: string
enum: [clone, empty]
description: |
Default Git workspace setup for agents on the provisioned instance. `empty` runs `git init` without a remote;
omit git URL and credentials when using empty mode.
gitRepositoryUrl:
type: string
description: Optional Git repository URL for agent workspace
description: Optional Git repository URL for agent workspace (clone mode)
gitUsername:
type: string
gitToken:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GitRepositorySetupMode } from '@forepath/framework/backend/feature-agent-manager';
import { AuthenticationType } from '@forepath/identity/backend';
import { Type } from 'class-transformer';
import { IsEnum, IsIn, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
Expand Down Expand Up @@ -56,6 +57,12 @@ export class ProvisionServerDto {
@Max(65535)
agentWsPort?: number;

@IsOptional()
@IsEnum(GitRepositorySetupMode, {
message: 'Git repository setup mode must be one of: clone, empty',
})
gitRepositorySetupMode?: GitRepositorySetupMode;

@IsOptional()
@IsString({ message: 'Git repository URL must be a string' })
gitRepositoryUrl?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ContainerType,
CreateAgentDto,
CreateAgentResponseDto,
GitRepositorySetupMode,
UpdateAgentDto,
} from '@forepath/framework/backend/feature-agent-manager';
import { AuthenticationType, ClientAgentCredentialsService, ClientEntity } from '@forepath/identity/backend';
Expand Down Expand Up @@ -303,6 +304,30 @@ describe('ClientAgentProxyService', () => {
mockCreateAgentResponse.password,
);
});
it('should forward gitRepositorySetupMode when creating an agent', async () => {
const createDto: CreateAgentDto = {
name: 'Empty Agent',
gitRepositorySetupMode: GitRepositorySetupMode.EMPTY,
};

clientsRepository.findByIdOrThrow.mockResolvedValue(mockClientEntity);
mockedAxios.request.mockResolvedValue({
data: mockCreateAgentResponse,
status: 201,
statusText: 'Created',
headers: {},
config: {} as any,
});

await service.createClientAgent('client-uuid', createDto);

expect(mockedAxios.request).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
data: createDto,
}),
);
});
});

describe('updateClientAgent', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GitRepositorySetupMode } from '@forepath/framework/backend/feature-agent-manager';
import { AuthenticationType, ClientEntity } from '@forepath/identity/backend';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
Expand Down Expand Up @@ -234,6 +235,32 @@ describe('ProvisioningService', () => {
);
});

it('should emit GIT_REPOSITORY_SETUP_MODE for empty git provisioning and omit clone credentials', async () => {
const emptyGitDto: ProvisionServerDto = {
...provisionDto,
gitRepositorySetupMode: GitRepositorySetupMode.EMPTY,
gitRepositoryUrl: 'https://github.com/user/repo.git',
gitUsername: 'user',
gitToken: 'token',
};

mockProvisioningProviderFactory.hasProvider.mockReturnValue(true);
mockProvisioningProviderFactory.getProvider.mockReturnValue(mockProvider);
(mockProvider.provisionServer as jest.Mock).mockResolvedValue(mockProvisionedServer);
mockClientsService.create.mockResolvedValue(mockClient);
mockProvisioningReferencesRepository.create.mockResolvedValue(mockProvisioningReference);

await service.provisionServer(emptyGitDto);

const provisionCall = (mockProvider.provisionServer as jest.Mock).mock.calls[0][0];
const userData = Buffer.from(provisionCall.userData, 'base64').toString('utf-8');

expect(userData).toContain('GIT_REPOSITORY_SETUP_MODE: empty');
expect(userData).not.toContain('GIT_REPOSITORY_URL:');
expect(userData).not.toContain('GIT_USERNAME:');
expect(userData).not.toContain('GIT_TOKEN:');
});

it('should use provided API key if given', async () => {
const dtoWithApiKey: ProvisionServerDto = {
...provisionDto,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { randomBytes } from 'crypto';

import { GitRepositorySetupMode } from '@forepath/framework/backend/feature-agent-manager';
import { AuthenticationType, UserRole } from '@forepath/identity/backend';
import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';

Expand Down Expand Up @@ -62,7 +63,14 @@ export class ProvisioningService {
authenticationType: AuthenticationType,
apiKey?: string,
keycloakConfig?: { clientId: string; clientSecret: string; realm?: string; authServerUrl?: string },
gitConfig?: { repositoryUrl?: string; username?: string; token?: string; password?: string; privateKey?: string },
gitConfig?: {
setupMode?: GitRepositorySetupMode;
repositoryUrl?: string;
username?: string;
token?: string;
password?: string;
privateKey?: string;
},
cursorApiKey?: string,
agentDefaultImage?: string,
autoEnrichEnabledGlobal?: string,
Expand Down Expand Up @@ -94,24 +102,28 @@ export class ProvisioningService {
// Build GIT environment variables
const gitEnvVars: string[] = [];

if (gitConfig?.repositoryUrl) {
gitEnvVars.push(`GIT_REPOSITORY_URL: ${gitConfig.repositoryUrl}`);
}
if (gitConfig?.setupMode === GitRepositorySetupMode.EMPTY) {
gitEnvVars.push(`GIT_REPOSITORY_SETUP_MODE: ${GitRepositorySetupMode.EMPTY}`);
} else if (gitConfig) {
if (gitConfig.repositoryUrl) {
gitEnvVars.push(`GIT_REPOSITORY_URL: ${gitConfig.repositoryUrl}`);
}

if (gitConfig?.username) {
gitEnvVars.push(`GIT_USERNAME: ${gitConfig.username}`);
}
if (gitConfig.username) {
gitEnvVars.push(`GIT_USERNAME: ${gitConfig.username}`);
}

if (gitConfig?.token) {
gitEnvVars.push(`GIT_TOKEN: ${gitConfig.token}`);
}
if (gitConfig.token) {
gitEnvVars.push(`GIT_TOKEN: ${gitConfig.token}`);
}

if (gitConfig?.password) {
gitEnvVars.push(`GIT_PASSWORD: ${gitConfig.password}`);
}
if (gitConfig.password) {
gitEnvVars.push(`GIT_PASSWORD: ${gitConfig.password}`);
}

if (gitConfig?.privateKey) {
gitEnvVars.push(`GIT_PRIVATE_KEY: ${gitConfig.privateKey}`);
if (gitConfig.privateKey) {
gitEnvVars.push(`GIT_PRIVATE_KEY: ${gitConfig.privateKey}`);
}
}

// Build cursor agent environment variables (only add if provided)
Expand Down Expand Up @@ -363,19 +375,22 @@ DOCKER_COMPOSE_EOF
authServerUrl: provisionServerDto.keycloakAuthServerUrl,
}
: undefined,
provisionServerDto.gitRepositoryUrl ||
provisionServerDto.gitUsername ||
provisionServerDto.gitToken ||
provisionServerDto.gitPassword ||
provisionServerDto.gitPrivateKey
? {
repositoryUrl: provisionServerDto.gitRepositoryUrl,
username: provisionServerDto.gitUsername,
token: provisionServerDto.gitToken,
password: provisionServerDto.gitPassword,
privateKey: provisionServerDto.gitPrivateKey,
}
: undefined,
provisionServerDto.gitRepositorySetupMode === GitRepositorySetupMode.EMPTY
? { setupMode: GitRepositorySetupMode.EMPTY }
: provisionServerDto.gitRepositoryUrl ||
provisionServerDto.gitUsername ||
provisionServerDto.gitToken ||
provisionServerDto.gitPassword ||
provisionServerDto.gitPrivateKey
? {
setupMode: GitRepositorySetupMode.CLONE,
repositoryUrl: provisionServerDto.gitRepositoryUrl,
username: provisionServerDto.gitUsername,
token: provisionServerDto.gitToken,
password: provisionServerDto.gitPassword,
privateKey: provisionServerDto.gitPrivateKey,
}
: undefined,
provisionServerDto.cursorApiKey,
provisionServerDto.agentDefaultImage,
provisionServerDto.autoEnrichEnabledGlobal,
Expand Down
10 changes: 8 additions & 2 deletions libs/domains/framework/backend/feature-agent-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,9 +711,15 @@ nx test framework-backend-feature-agent-manager --coverage

### Git Repository Environment Variables

These environment variables are required for git repository cloning when creating agents:
When creating agents, the workspace Git repository is initialized in one of two modes (see `gitRepositorySetupMode` on `CreateAgentDto` or `GIT_REPOSITORY_SETUP_MODE`):

- `GIT_REPOSITORY_URL` - HTTPS or SSH URL of the git repository to clone (e.g., `https://github.com/user/repo.git` or `git@github.com:user/repo.git`)
- **`clone` (default)** — clone a remote repository (requires URL and credentials below).
- **`empty`** — run `git init` in the provider workspace path with no remote attached.

Per-agent `gitRepositorySetupMode` on `CreateAgentDto` overrides the instance default (`GIT_REPOSITORY_SETUP_MODE`). If the field is omitted, the instance default applies (for example, a workspace provisioned or configured with `empty`).

- `GIT_REPOSITORY_SETUP_MODE` - Optional instance default: `clone` or `empty` (defaults to `clone` when unset)
- `GIT_REPOSITORY_URL` - HTTPS or SSH URL of the git repository to clone (required for clone mode unless provided per agent)
- `GIT_USERNAME` - Git username for authentication (**required for HTTPS URLs**)
- `GIT_TOKEN` - Git personal access token for authentication (preferred for HTTPS, or use `GIT_PASSWORD`)
- `GIT_PASSWORD` - Git password or token (alternative to `GIT_TOKEN` for HTTPS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ sequenceDiagram
Service->>Service: generatePassword()
Service->>Service: hashPassword(password)
Service->>Service: generateVolumePath()
Service->>Service: isSshRepository(GIT_REPOSITORY_URL)
alt HTTPS Repository
Service->>Service: resolve gitRepositorySetupMode
alt Empty repository (git init)
Service->>Docker: createContainer(name, env, volumes)
Note over Docker: GIT_REPOSITORY_SETUP_MODE=empty<br/>no GIT_REPOSITORY_URL
Docker-->>Service: containerId
Service->>Docker: sendCommandToContainer(containerId, "git init {path}")
Docker-->>Service: repository initialized
else Clone - HTTPS Repository
Service->>Service: isSshRepository(GIT_REPOSITORY_URL)
Service->>Docker: createContainer(name, env, volumes)
Note over Docker: Container created with:<br/>- GIT_REPOSITORY_URL<br/>- GIT_USERNAME<br/>- GIT_TOKEN/GIT_PASSWORD<br/>- CURSOR_API_KEY<br/>- Volume: /opt/agents/{uuid} -> /app
Docker-->>Service: containerId
Service->>Docker: createNetrcFile(containerId)
Note over Docker: Creates $HOME/.netrc<br/>(worker: /home/agenstra/.netrc)<br/>machine {domain}<br/> login {username}<br/> password {token}
Docker-->>Service: .netrc created
else SSH Repository
else Clone - SSH Repository
Service->>Service: prepareSshKeyPair(GIT_PRIVATE_KEY)
alt GIT_PRIVATE_KEY not set or invalid
Service-->>HTTP: BadRequestException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ sequenceDiagram
Service->>Service: generatePassword()
Service->>Service: hashPassword(password)
Service->>Service: generateVolumePath()
Service->>Service: isSshRepository(GIT_REPOSITORY_URL)
alt HTTPS Repository
Service->>Service: resolve gitRepositorySetupMode
alt Empty repository (git init)
Service->>Docker: createContainer(name, env, volumes)
Docker-->>Service: containerId
Service->>Docker: sendCommandToContainer(containerId, "git init {path}")
Docker-->>Service: repository initialized
else Clone - HTTPS Repository
Service->>Service: isSshRepository(GIT_REPOSITORY_URL)
Service->>Docker: createContainer(name, env, volumes)
Note over Docker: Container created with:<br/>- GIT_REPOSITORY_URL<br/>- GIT_USERNAME<br/>- GIT_TOKEN/GIT_PASSWORD<br/>- CURSOR_API_KEY<br/>- Volume mounted at /app
Docker-->>Service: containerId
Service->>Docker: createNetrcFile(containerId)
Note over Docker: Creates $HOME/.netrc<br/>(worker: /home/agenstra/.netrc)
Docker-->>Service: .netrc created
else SSH Repository
else Clone - SSH Repository
Service->>Service: prepareSshKeyPair(GIT_PRIVATE_KEY)
alt GIT_PRIVATE_KEY not set or invalid
Service-->>API: BadRequestException
Expand Down
Loading
Loading