Skip to content
Open
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
2 changes: 1 addition & 1 deletion .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sonar.cpd.exclusions=workspaces/*/packages/app/**, workspaces/*/packages/app-legacy/**, workspaces/*/packages/backend/**, workspaces/*/plugins/*test*/**, **/*.test.*, **/translations/*.ts, **/__fixtures__/*, **/playwright.config.ts
sonar.exclusions=workspaces/*/plugins/orchestrator-common/src/generated/**
sonar.exclusions=workspaces/*/plugins/orchestrator-common/src/generated/**, workspaces/x2a/**/openapi/generated/**

10 changes: 10 additions & 0 deletions workspaces/x2a/.changeset/chilly-needles-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a': patch
'@red-hat-developer-hub/backstage-plugin-x2a-mcp-extras': patch
'@red-hat-developer-hub/backstage-plugin-x2a-backend': patch
'@red-hat-developer-hub/backstage-plugin-x2a-common': patch
'@red-hat-developer-hub/backstage-plugin-x2a-node': patch
'@red-hat-developer-hub/backstage-plugin-x2a': patch
---

implemented adversarial agents
2 changes: 2 additions & 0 deletions workspaces/x2a/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ x2a:
# X2A convertor container image
image: ${X2A_KUBERNETES_IMAGE:-quay.io/x2ansible/x2a-convertor}
imageTag: ${X2A_KUBERNETES_IMAGE_TAG:-latest}
# Image pull policy — set to Never or IfNotPresent for locally-loaded kind images
imagePullPolicy: ${X2A_KUBERNETES_IMAGE_PULL_POLICY:-IfNotPresent}
# Auto-delete completed jobs after 24 hours (86400 seconds)
ttlSecondsAfterFinished: ${X2A_KUBERNETES_TTL_SECONDS:-86400}
# Resource requests and limits for job pods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type CreateAndInitProjectParams = {
targetRepoToken: string;
userPrompt?: string;
acceptedRuleIds?: string[];
adversarialAgentIds?: string[];
backstageToken?: string;
hostProviderMap: Map<string, ScmProviderName>;
logger: ActionLogger;
Expand All @@ -45,6 +46,7 @@ export const createAndInitProject = async (
targetRepoToken,
userPrompt,
acceptedRuleIds,
adversarialAgentIds,
backstageToken: token,
logger,
} = params;
Expand All @@ -58,6 +60,7 @@ export const createAndInitProject = async (
sourceRepoBranch: row.sourceRepoBranch,
targetRepoBranch: row.targetRepoBranch,
acceptedRuleIds,
adversarialAgentIds,
};

logger.info(`Creating project "${row.name}" (${JSON.stringify(body)})`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async function handleManualCreation(params: {
targetRepoBranch: string;
userPrompt?: string;
acceptedRuleIds?: string;
adversarialAgentIds?: string[];
};
secrets: Record<string, string> | undefined;
api: DefaultApiClient;
Expand Down Expand Up @@ -187,6 +188,7 @@ async function handleManualCreation(params: {
targetRepoToken,
userPrompt: input.userPrompt,
acceptedRuleIds,
adversarialAgentIds: input.adversarialAgentIds,
backstageToken: token,
hostProviderMap,
logger,
Expand Down Expand Up @@ -365,6 +367,12 @@ export function createProjectAction(
description: 'JSON-stringified array of accepted rule UUIDs',
})
.optional(),
adversarialAgentIds: z
.array(z.string(), {
description:
'UUIDs of adversarial agents to attach to the project',
})
.optional(),
csvContent: z.string().optional(),
}),
z.object({
Expand Down Expand Up @@ -397,6 +405,7 @@ export function createProjectAction(
targetRepoUrl: z.string().optional(),
targetRepoBranch: z.string().optional(),
acceptedRuleIds: z.string().optional(),
adversarialAgentIds: z.array(z.string()).optional(),
}),
]),
output: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ spec:
ui:order:
- userPrompt
- acceptedRuleIds
- adversarialAgentIds
properties:
userPrompt:
title: User prompt
Expand All @@ -193,6 +194,12 @@ spec:
title: Project rules
type: string
ui:field: RulesAcceptance
adversarialAgentIds:
title: Adversarial agents
type: array
items:
type: string
ui:field: AdversarialAgentsPicker

steps:
- id: createProject
Expand All @@ -211,6 +218,7 @@ spec:
targetRepoBranch: ${{ parameters.targetRepoBranch }}
userPrompt: ${{ parameters.userPrompt }}
acceptedRuleIds: ${{ parameters.acceptedRuleIds }}
adversarialAgentIds: ${{ parameters.adversarialAgentIds }}

# Outputs are displayed to the user after a successful execution of the template.
output:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { Knex } from 'knex';

const EXTENDED_PHASES = [
'init',
'analyze',
'migrate',
'publish',
'adversarial-analyze',
'adversarial-migrate',
];
const ORIGINAL_PHASES = ['init', 'analyze', 'migrate', 'publish'];

function createJobsTable(table: Knex.CreateTableBuilder, phases: string[]) {
table.uuid('id').primary();
table.text('log');
table.timestamp('started_at').notNullable();
table.timestamp('finished_at');
table
.string('status')
.notNullable()
.defaultTo('pending')
.checkIn(['pending', 'running', 'success', 'error', 'cancelled']);
table.string('phase').notNullable().defaultTo('init').checkIn(phases);
table.text('error_details');
table.text('telemetry');
table.string('k8s_job_name');
table.string('callback_token');
table.string('commit_id').nullable();
table
.uuid('project_id')
.notNullable()
.references('id')
.inTable('projects')
.onDelete('CASCADE')
.index();
table
.uuid('module_id')
.nullable()
.references('id')
.inTable('modules')
.onDelete('CASCADE')
.index();
table.index('started_at');
table.index('finished_at');
table.index('status');
table.index('phase');
table.index('k8s_job_name');
}

async function recreateJobsTableSqlite(
knex: Knex,
phases: string[],
): Promise<void> {
await knex.schema.raw('PRAGMA foreign_keys = OFF');
try {
await knex.schema.createTable('jobs_new', table =>
createJobsTable(table, phases),
);
await knex.schema.raw('INSERT INTO jobs_new SELECT * FROM jobs');
await knex.schema.dropTable('jobs');
await knex.schema.raw('ALTER TABLE jobs_new RENAME TO jobs');
} finally {
await knex.schema.raw('PRAGMA foreign_keys = ON');
}
}

/**
* Creates the adversarial_agents table, adds adversarial_agents column to projects,
* and expands the jobs.phase CHECK constraint to include adversarial phases.
*
* @public
*/
export async function up(knex: Knex): Promise<void> {
const client = knex.client.config.client;

if (client === 'better-sqlite3') {
await recreateJobsTableSqlite(knex, EXTENDED_PHASES);
} else {
// PostgreSQL: drop and recreate the named constraint
await knex.raw(
`ALTER TABLE jobs DROP CONSTRAINT IF EXISTS jobs_phase_check`,
);
await knex.raw(
`ALTER TABLE jobs ADD CONSTRAINT jobs_phase_check CHECK (phase IN ('init', 'analyze', 'migrate', 'publish', 'adversarial-analyze', 'adversarial-migrate'))`,
);
}

await knex.schema.createTable('adversarial_agents', table => {
table.uuid('id').primary();
table.string('name', 100).notNullable();
table.text('prompt').notNullable();
table.text('phases').notNullable();
table.boolean('critical').notNullable().defaultTo(false);
table.string('created_by', 255).notNullable();
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());

table.index('updated_at');
table.index('name');
});

await knex.schema.alterTable('projects', table => {
table.text('adversarial_agents').nullable();
});
}

/**
* Drops adversarial_agents column from projects, drops adversarial_agents table,
* and restores the original jobs.phase CHECK constraint.
*
* @public
*/
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('projects', table => {
table.dropColumn('adversarial_agents');
});

await knex.schema.dropTable('adversarial_agents');

const client = knex.client.config.client;

if (client === 'better-sqlite3') {
await recreateJobsTableSqlite(knex, ORIGINAL_PHASES);
} else {
await knex.raw(
`ALTER TABLE jobs DROP CONSTRAINT IF EXISTS jobs_phase_check`,
);
await knex.raw(
`ALTER TABLE jobs ADD CONSTRAINT jobs_phase_check CHECK (phase IN ('init', 'analyze', 'migrate', 'publish'))`,
);
}
}
22 changes: 22 additions & 0 deletions workspaces/x2a/plugins/x2a-backend/src/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,28 @@ const getX2aDatabaseServiceMock = (): typeof x2aDatabaseServiceRef.T => ({
getAcceptedRulesForProject: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
// adversarial agents
createAdversarialAgent: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
listAdversarialAgents: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
getAdversarialAgent: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
updateAdversarialAgent: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
deleteAdversarialAgent: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
attachAdversarialAgentsToProject: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
getAdversarialAgentsForProject: jest
.fn()
.mockRejectedValue(new NotAllowedError('mock error')),
});

const getKubeServiceMock = () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,28 @@ export class GitRepositoryResolver {
),
};
}

resolveTargetOnly(params: {
project: Pick<
ResolveParams['project'],
'targetRepoUrl' | 'targetRepoBranch'
>;
targetRepoAuth?: { token: string };
}): GitRepository {
const targetToken =
params.targetRepoAuth?.token ??
this.config.getOptionalString('x2a.git.targetRepo.token');

if (!targetToken) {
throw new InputError(
'Target repository token is required. Provide it in the request or configure x2a.git.targetRepo.token.',
);
}

return new GitRepository(
params.project.targetRepoUrl,
params.project.targetRepoBranch,
targetToken,
);
}
}
Loading
Loading