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
45 changes: 44 additions & 1 deletion apps/backend-agent-manager/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,60 @@
* This is only a minimal backend to get started.
*/

import {
WORKSPACE_CONFIGURATION_ENV_BY_SETTING,
WorkspaceConfigurationOverrideEntity,
type WorkspaceConfigurationSettingKey,
} from '@forepath/framework/backend/feature-agent-manager';
import { assertProductionEncryptionKeyOrExit } from '@forepath/shared/backend';
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { DataSource } from 'typeorm';

import { AppModule } from './app/app.module';
import { typeormConfig } from './typeorm.config';
import { typeormConfig, typeormConfigForConfigurationOverrides } from './typeorm.config';

async function preloadWorkspaceConfigurationOverrides(logger: Logger): Promise<void> {
const preloadDataSource = new DataSource(typeormConfigForConfigurationOverrides);

try {
await preloadDataSource.initialize();

if (!typeormConfig.synchronize && typeormConfig.migrations?.length) {
logger.log('🔄 Running pending config migrations...');
await preloadDataSource.runMigrations();
logger.log('✅ Config migrations completed successfully');
} else if (typeormConfig.synchronize) {
logger.log('ℹ️ Schema synchronization enabled - config migrations skipped');
}

logger.log('🔄 Loading config overrides...');

const overrides = await preloadDataSource.getRepository(WorkspaceConfigurationOverrideEntity).find();

for (const override of overrides) {
const settingKey = override.settingKey as WorkspaceConfigurationSettingKey;
const envVarName = WORKSPACE_CONFIGURATION_ENV_BY_SETTING[settingKey];

if (!envVarName) {
continue;
}

process.env[envVarName] = override.value;
}

logger.log(`✅ Loaded ${overrides.length} config override(s)`);
} finally {
if (preloadDataSource.isInitialized) {
await preloadDataSource.destroy();
}
}
}

async function bootstrap() {
assertProductionEncryptionKeyOrExit(new Logger('EncryptionKey'));
await preloadWorkspaceConfigurationOverrides(new Logger('WorkspaceConfigurationOverridesBootstrap'));

const app = await NestFactory.create(AppModule);
// Configure CORS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateWorkspaceConfigurationOverridesTable1773000000000 implements MigrationInterface {
name = 'CreateWorkspaceConfigurationOverridesTable1773000000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "workspace_configuration_overrides" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"setting_key" varchar(64) NOT NULL,
"value" text NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_workspace_configuration_overrides_id" PRIMARY KEY ("id")
)
`);

await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "IDX_workspace_configuration_overrides_setting_key_unique"
ON "workspace_configuration_overrides" ("setting_key")
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_workspace_configuration_overrides_setting_key_unique"`);
await queryRunner.query(`DROP TABLE IF EXISTS "workspace_configuration_overrides"`);
}
}
7 changes: 7 additions & 0 deletions apps/backend-agent-manager/src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DeploymentConfigurationEntity,
DeploymentRunEntity,
RegexFilterRuleEntity,
WorkspaceConfigurationOverrideEntity,
} from '@forepath/framework/backend';
import { DataSource, DataSourceOptions } from 'typeorm';

Expand All @@ -32,6 +33,7 @@ export const typeormConfig: DataSourceOptions = {
DeploymentConfigurationEntity,
DeploymentRunEntity,
RegexFilterRuleEntity,
WorkspaceConfigurationOverrideEntity,
],
// Migration paths:
// - In development with TypeScript: use path from workspace root
Expand All @@ -47,6 +49,11 @@ export const typeormConfig: DataSourceOptions = {
logging: process.env.NODE_ENV === 'development',
};

export const typeormConfigForConfigurationOverrides: DataSourceOptions = {
...typeormConfig,
entities: [WorkspaceConfigurationOverrideEntity],
};

/**
* TypeORM DataSource configuration for CLI operations.
* This file is used by TypeORM CLI for generating and running migrations.
Expand Down
24 changes: 24 additions & 0 deletions apps/frontend-agent-console/src/i18n/messages.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -3942,6 +3942,30 @@
<source>Remove relation</source>
<target>Beziehung entfernen</target>
</trans-unit>
<trans-unit id="featureChat-manageWorkspaceConfigurationTitle" datatype="html">
<source>Manage workspace configuration</source>
<target>Workspace-Konfiguration verwalten</target>
</trans-unit>
<trans-unit id="featureChat-manageWorkspaceConfiguration" datatype="html">
<source>Manage Workspace Configuration</source>
<target>Workspace-Konfiguration verwalten</target>
</trans-unit>
<trans-unit id="featureChat-workspaceConfigurationSettings" datatype="html">
<source>Workspace Configuration Settings</source>
<target>Workspace-Konfigurationseinstellungen</target>
</trans-unit>
<trans-unit id="featureChat-workspaceConfigurationValuePlaceholder" datatype="html">
<source>Enter override value</source>
<target>Override-Wert eingeben</target>
</trans-unit>
<trans-unit id="featureChat-saveWorkspaceConfigurationTitle" datatype="html">
<source>Save override</source>
<target>Override speichern</target>
</trans-unit>
<trans-unit id="featureChat-deleteWorkspaceConfigurationOverrideTitle" datatype="html">
<source>Delete override</source>
<target>Override löschen</target>
</trans-unit>
</body>
</file>
</xliff>
18 changes: 18 additions & 0 deletions apps/frontend-agent-console/src/i18n/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2955,6 +2955,24 @@
<trans-unit id="featureUserManager-delete" datatype="html">
<source>Delete</source>
</trans-unit>
<trans-unit id="featureChat-manageWorkspaceConfigurationTitle" datatype="html">
<source>Manage workspace configuration</source>
</trans-unit>
<trans-unit id="featureChat-manageWorkspaceConfiguration" datatype="html">
<source>Manage Workspace Configuration</source>
</trans-unit>
<trans-unit id="featureChat-workspaceConfigurationSettings" datatype="html">
<source>Workspace Configuration Settings</source>
</trans-unit>
<trans-unit id="featureChat-workspaceConfigurationValuePlaceholder" datatype="html">
<source>Enter override value</source>
</trans-unit>
<trans-unit id="featureChat-saveWorkspaceConfigurationTitle" datatype="html">
<source>Save override</source>
</trans-unit>
<trans-unit id="featureChat-deleteWorkspaceConfigurationOverrideTitle" datatype="html">
<source>Delete override</source>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
sequenceDiagram
participant Admin as WorkspaceOrGlobalAdmin
participant Controller as AgentController
participant Manager as AgentManager
participant Db as OverridesDb
participant Env as RuntimeProcessEnv
participant Agents as WorkspaceEnvironments
participant Docker as DockerEngine

Admin->>Controller: PUT /clients/{id}/configuration-overrides/{settingKey}
Controller->>Controller: ensureWorkspaceManagementAccess
Controller->>Manager: PUT /api/configuration-overrides/{settingKey}
Manager->>Db: upsert encrypted override value
Manager->>Env: process.env[envVarName]=overrideValue
Manager->>Agents: reconcile relevant environment containers
Agents->>Docker: recreate containers using changed env keys
Manager-->>Controller: setting {source=override}
Controller-->>Admin: 200 response

Admin->>Controller: DELETE /clients/{id}/configuration-overrides/{settingKey}
Controller->>Controller: ensureWorkspaceManagementAccess
Controller->>Manager: DELETE /api/configuration-overrides/{settingKey}
Manager->>Db: delete override row
Manager->>Env: delete process.env[envVarName]
Manager->>Agents: reconcile relevant environment containers
Agents->>Docker: recreate containers removing changed env keys
Manager-->>Controller: 204
Controller-->>Admin: 204
Original file line number Diff line number Diff line change
Expand Up @@ -1863,6 +1863,93 @@ paths:
description: No client access, or workspace management rights required
'404':
description: Client, agent, or environment variable not found
/clients/{id}/configuration-overrides:
get:
summary: List effective workspace configuration overrides (proxied)
description: Requires workspace management rights.
operationId: listClientWorkspaceConfigurationOverrides
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
description: The UUID of the client
responses:
'200':
description: Effective workspace configuration settings
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/WorkspaceConfigurationSettingResponseDto'
'403':
description: Workspace management rights required
'404':
description: Client not found
/clients/{id}/configuration-overrides/{settingKey}:
put:
summary: Create or update a workspace configuration override (proxied)
description: Requires workspace management rights.
operationId: upsertClientWorkspaceConfigurationOverride
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
description: The UUID of the client
- in: path
name: settingKey
required: true
schema:
$ref: '#/components/schemas/WorkspaceConfigurationSettingKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpsertWorkspaceConfigurationOverrideDto'
responses:
'200':
description: Updated effective workspace configuration setting
content:
application/json:
schema:
$ref: '#/components/schemas/WorkspaceConfigurationSettingResponseDto'
'400':
description: Invalid request or unsupported setting key
'403':
description: Workspace management rights required
'404':
description: Client not found
delete:
summary: Delete a workspace configuration override (proxied)
description: Requires workspace management rights.
operationId: deleteClientWorkspaceConfigurationOverride
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
description: The UUID of the client
- in: path
name: settingKey
required: true
schema:
$ref: '#/components/schemas/WorkspaceConfigurationSettingKey'
responses:
'204':
description: Override deleted successfully
'403':
description: Workspace management rights required
'404':
description: Client not found
/clients/{id}/agents/{agentId}/vcs/status:
get:
summary: Get git status (proxied)
Expand Down Expand Up @@ -4253,6 +4340,40 @@ components:
updatedAt:
type: string
format: date-time
WorkspaceConfigurationSettingKey:
type: string
enum:
[
gitRepositoryUrl,
gitUsername,
gitToken,
gitPassword,
gitPrivateKey,
cursorApiKey,
agentDefaultImage,
]
UpsertWorkspaceConfigurationOverrideDto:
type: object
required: [value]
properties:
value:
type: string
description: Override value
WorkspaceConfigurationSettingResponseDto:
type: object
required: [settingKey, envVarName, source, hasOverride]
properties:
settingKey:
$ref: '#/components/schemas/WorkspaceConfigurationSettingKey'
envVarName:
type: string
value:
type: string
source:
type: string
enum: [override, default_env, unset]
hasOverride:
type: boolean
AddClientUserDto:
type: object
required: [email, role]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ClientUsersRepository } from '@forepath/identity/backend';
import { Test, TestingModule } from '@nestjs/testing';

import { ClientsRepository } from '../repositories/clients.repository';
import { ClientWorkspaceConfigurationOverridesProxyService } from '../services/client-workspace-configuration-overrides-proxy.service';

import { ClientsConfigurationOverridesController } from './clients-configuration-overrides.controller';

jest.mock('@forepath/identity/backend', () => {
const actual = jest.requireActual('@forepath/identity/backend');

return {
...actual,
ensureClientAccess: jest.fn().mockResolvedValue(undefined),
ensureWorkspaceManagementAccess: jest.fn().mockResolvedValue(undefined),
};
});

describe('ClientsConfigurationOverridesController', () => {
let controller: ClientsConfigurationOverridesController;
let proxyService: jest.Mocked<ClientWorkspaceConfigurationOverridesProxyService>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ClientsConfigurationOverridesController],
providers: [
{
provide: ClientWorkspaceConfigurationOverridesProxyService,
useValue: {
getConfigurationOverrides: jest.fn().mockResolvedValue([]),
upsertConfigurationOverride: jest.fn().mockResolvedValue({ settingKey: 'gitToken', value: 'abc' }),
deleteConfigurationOverride: jest.fn().mockResolvedValue(undefined),
},
},
{ provide: ClientsRepository, useValue: {} },
{ provide: ClientUsersRepository, useValue: {} },
],
}).compile();

controller = module.get(ClientsConfigurationOverridesController);
proxyService = module.get(ClientWorkspaceConfigurationOverridesProxyService);
});

it('proxies list', async () => {
await controller.getConfigurationOverrides('8f6e8fc8-7a18-4f96-bd81-cd4fca6e8ea8');
expect(proxyService.getConfigurationOverrides).toHaveBeenCalled();
});
});
Loading
Loading