Skip to content

Pluginstuff#84

Open
deacon-mp wants to merge 12 commits into
masterfrom
Pluginstuff
Open

Pluginstuff#84
deacon-mp wants to merge 12 commits into
masterfrom
Pluginstuff

Conversation

@deacon-mp
Copy link
Copy Markdown
Contributor

Description

DO AFTER ADVERSARY VIEW
Hot reload plugins with UI

Type of change

navbar and

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces UI and client-side plumbing for plugin enable/disable workflows that trigger a Caldera restart (with a global “rebuild/reconnect” overlay), and expands the “Adversaries” area into an “Automation” editor that supports importing profiles and editing per-step executor facts.

Changes:

  • Added Vite dev-server proxying for auth/core API/plugin routes and configurable backend URL.
  • Implemented plugin enable/disable restart flow: axios interceptor handling, global restart overlay/polling, and modal/navigation updates.
  • Reworked adversary/automation editing to support importing profiles, editing ability instances, and executor-facts normalization utilities.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
vite.config.js Adds env-driven backend URL, dev proxy rules, and server config for local development.
src/views/LoginView.vue Adds form submit handler and browser autocomplete hints for credentials.
src/views/AdversariesView.vue.bkp Backup copy of prior adversaries view (added in PR).
src/views/AdversariesView.vue Reworks view into “Automation” editor with import + per-step ability editing modal.
src/views/AbilitiesView.vue Adjusts selected ability initialization and modal props/flags for editor reuse.
src/utils/executorUtils.js Adds utilities to rebuild/normalize executor facts for load/save paths.
src/stores/coreStore.js Changes default plugin visibility setting and adjusts available-plugins error handling.
src/stores/coreDisplayStore.js Adds restart flag and expands core plugin-modal UI state (mode, selections).
src/stores/authStore.js Updates auth status tracking to set isUserAuthenticated on success/failure.
src/stores/adversaryStore.js.bkp Backup copy of prior adversary store (added in PR).
src/stores/adversaryStore.js Adds richer adversary load/save logic, step UUIDs, and executor-facts normalization.
src/main.js Adds axios response interceptor and adds spinner icon for restart overlay UX.
src/components/operations/CreateModal.vue Refactors operation creation logic (payload builder + jitter normalization).
src/components/core/PluginModal.vue Updates modal to support enable + multi-disable flows and restart timing.
src/components/core/Navigation.vue Adds “Disable a Plugin” entry and wires modal state for enable/disable flows.
src/components/adversaries/ImportModal.vue.bkp Backup copy of prior YAML import modal (added in PR).
src/components/adversaries/ImportModal.vue Replaces custom YAML parsing with js-yaml + emits imported profile to parent.
src/components/adversaries/FactBreakdownModal.vue Replaces tag-based breakdown with detailed usage tables tied to fact sources.
src/components/adversaries/DetailsTable.vue.bkp Backup copy of prior details table (added in PR).
src/components/adversaries/DetailsTable.vue Major refactor: local abilities state, DnD ordering, fact source dropdown, warning icons.
src/components/adversaries/DeleteAdversaryConfirmationModal.vue.bkp Backup copy of prior delete confirmation modal (added in PR).
src/components/adversaries/DeleteAdversaryConfirmationModal.vue Updates store import paths to alias-based imports.
src/components/abilities/CreateEditAbility.vue.bkp Backup copy of prior ability editor (added in PR).
src/components/abilities/CreateEditAbility.vue Expands editor to support adversary-step instances + trait/fact selection UI.
src/App.vue Adds global restart overlay with delayed health polling + reconnect option; moves data refresh to route watcher.
package-lock.json Lockfile updates reflecting dependency metadata/structure changes.
Comments suppressed due to low confidence (1)

src/stores/coreDisplayStore.js:12

  • modals.automationImport is used elsewhere, but the initial modals state doesn’t define it. Add an explicit automationImport: false flag under modals to keep modal state shape consistent.
      modals: {
        agents: {
          showDeploy: false,
          showConfig: false,
          showDetails: false,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'row-hover-below': ability.step_uuid === tableDragHoverId.value && tableDragHoverId.value != undefined && tableDragEndIndex.value > tableDragTargetIndex.value,
'orange-row': needsParser.value.indexOf(ability.name) > -1,
'row-hover': ability.step_uuid === tableDragHoverId.value && tableDragHoverId.value != undefined,
'red-row-unclickable': undefinedAbilities.value.indexOf(ability.step_uuid) > -1
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRowClass() checks undefinedAbilities against ability.step_uuid, but undefinedAbilities is populated with ability_id values. Align these so the undefined-ability styling can actually trigger.

Suggested change
'red-row-unclickable': undefinedAbilities.value.indexOf(ability.step_uuid) > -1
'red-row-unclickable': undefinedAbilities.value.indexOf(ability.ability_id) > -1

Copilot uses AI. Check for mistakes.
this.modals.core.selectedPlugin = pluginName;
this.modals.core.showPluginPopup = true;
if (!modals.value?.core) return;

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

promptToEnablePlugin() should explicitly set modals.value.core.mode = 'enable' before opening the modal. Otherwise, after using the disable flow, the modal can reopen in “disable” mode when prompting to enable a plugin.

Suggested change
modals.value.core.mode = "enable";

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +41
const { agents } = storeToRefs(agentStore);
const { abilities } = storeToRefs(abilityStore);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agents and abilities refs are created but never used in this view. Removing them avoids lint warnings and reduces reactive overhead.

Suggested change
const { agents } = storeToRefs(agentStore);
const { abilities } = storeToRefs(abilityStore);

Copilot uses AI. Check for mistakes.
button.delete(@click.stop="deleteAbility(index)")

.container.has-text-centered(
v-if="abilitiesReady && Array.isArray(localAbilities.value) && !localAbilities.value.length"
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the template, localAbilities is a ref but Vue templates auto-unwrap refs. Using localAbilities.value inside expressions (e.g. Array.isArray(localAbilities.value)) will be undefined and the empty-state block will never render. Use localAbilities (no .value) in template expressions.

Suggested change
v-if="abilitiesReady && Array.isArray(localAbilities.value) && !localAbilities.value.length"
v-if="abilitiesReady && Array.isArray(localAbilities) && !localAbilities.length"

Copilot uses AI. Check for mistakes.
});
}
} else {
undefinedAbilities.value.push(ability.ability_id);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefinedAbilities is populated with ability.ability_id (and in the else branch that value will be undefined), but later row styling checks against ability.step_uuid. Track a consistent identifier for “undefined ability” (either push step_uuid or check against ability_id).

Suggested change
undefinedAbilities.value.push(ability.ability_id);
undefinedAbilities.value.push(ability.step_uuid);

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +32
const router = useRouter();

const adversaryStore = useAdversaryStore();
const { adversaries, selectedAdversary } = storeToRefs(adversaryStore);
const coreStore = useCoreStore();
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router and coreStore are instantiated but never used in this view. If linting is enforced, this will fail CI. Remove unused store/router setup (or use them).

Copilot uses AI. Check for mistakes.
@@ -1,5 +1,5 @@
<script setup>
import { inject } from "vue";
import { inject, onMounted, watch, ref, computed } from "vue";
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onMounted, watch, ref, and computed are imported but not used in this component. If linting is enforced, this will fail CI. Remove unused imports.

Suggested change
import { inject, onMounted, watch, ref, computed } from "vue";
import { inject } from "vue";

Copilot uses AI. Check for mistakes.
Comment thread src/views/LoginView.vue
)
span.icon.is-small.is-left
font-awesome-icon(icon="fas fa-lock")
button.button.fancy-button.is-fullwidth(type="submit" @click="handleLogin") Log In
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This button submits the form (type="submit") and also calls handleLogin on click while the form already has @submit.prevent. That can trigger the login flow twice. Remove the @click handler and rely on the form submit handler only.

Suggested change
button.button.fancy-button.is-fullwidth(type="submit" @click="handleLogin") Log In
button.button.fancy-button.is-fullwidth(type="submit") Log In

Copilot uses AI. Check for mistakes.
Comment on lines +277 to +282
// Ensure abilities are available
if (!abilityStoreInstance.abilities.length) {
await abilityStoreInstance.getAbilities($api);

}

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadAdversaryFromProfile() calls abilityStoreInstance.getAbilities($api), but $api isn’t defined in this store’s scope. Pass $api into this action (and update callers) or remove the implicit fetch and require the caller to preload abilities.

Suggested change
// Ensure abilities are available
if (!abilityStoreInstance.abilities.length) {
await abilityStoreInstance.getAbilities($api);
}

Copilot uses AI. Check for mistakes.
*/
function handleClickOutside(event) {
for (const [index, executor] of (abilityToEdit.value.executors || []).entries()) {
const key = getExecutorKey(executor, index);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superfluous argument passed to function getExecutorKey.

Suggested change
const key = getExecutorKey(executor, index);
const key = getExecutorKey(executor);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 20 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 7 to 29
@@ -23,6 +23,9 @@ export const useCoreDisplayStore = defineStore("coreDisplayStore", {
core: {
showPluginPopup: false,
selectedPlugin: "",
mode: "enable",
selectedPlugins: [],
availableToDisable: []
},
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modals.automationImport is used in AdversariesView.vue, but this store’s initial state doesn’t define it. Please add an explicit automationImport flag under modals (or nest it under modals.adversaries) so the modal state is declared and survives store resets.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to 12
import { useCoreDisplayStore } from "../../stores/coreDisplayStore";
import { useCoreStore } from "../../stores/coreStore";

const $api = inject("$api");
const coreStore = useCoreStore();

const coreDisplayStore = useCoreDisplayStore();
const { modals } = storeToRefs(coreDisplayStore);

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coreStore is imported/instantiated but never used in this component. Please remove the unused import/variable to avoid lint failures and keep the module clean.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +71
export function buildExecutorsFromFacts(originalExecutors, executorFactsMap = {}) {
if (!Array.isArray(originalExecutors)) return [];

return originalExecutors.map((executor) => {
const platformFacts = executorFactsMap?.[executor.platform] || [];

return {
...cloneDeep(executor),
executor_facts: cloneDeep(platformFacts),
payloads: Array.isArray(executor.payloads) ? executor.payloads : [],
};
});
}

/**
* Normalizes unstructured executor_facts using ability executor info
*
* @param {Object} step - Adversary step with optional executor_facts
* @param {Array} abilityExecutors - Ability.executors list to match
* @returns {Object} Step with normalized executor_facts
*/
export function normalizeStepExecutorFacts(step, abilityExecutors) {
const executorFacts = step?.metadata?.executor_facts;
if (!executorFacts || typeof executorFacts !== "object") return step;

return {
...step,
metadata: {
...step.metadata,
executor_facts: executorFacts,
},
};
}

// utils/executorUtils.js
export function normalizeExecutorFactsForSave(executorFacts = {}) {
const normalized = {};

for (const [key, facts] of Object.entries(executorFacts)) {
if (!Array.isArray(facts) || facts.length === 0) continue;

// UI keys supported:
// - uuid::linux
// - linux_sh_0
// - linux (already backend style)
let platform = key;

if (key.includes("::")) {
platform = key.split("::").pop(); // uuid::linux -> linux
} else if (key.includes("_")) {
platform = key.split("_")[0]; // linux_sh_0 -> linux
}

normalized[platform] ??= [];
normalized[platform].push(
...facts.map(({ trait, value }) => ({ trait, value }))
);
}

return normalized;
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New executorUtils functions don’t have unit tests. The repo already has src/tests/utils.test.js; please add coverage for normalizeExecutorFactsForSave (key parsing/merging) and buildExecutorsFromFacts (fact injection, payload defaulting) to prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +76
<script setup>
import { ref, inject } from "vue";
import { useCoreDisplayStore } from "../../stores/coreDisplayStore";
import { storeToRefs } from "pinia";
import { useAdversaryStore } from "../../stores/adversaryStore";

const $api = inject("$api");

const adversaryStore = useAdversaryStore();
const coreDisplayStore = useCoreDisplayStore();
const { modals } = storeToRefs(coreDisplayStore);

let importFile = ref({});
let importFileContent = ref({});

function uploadImportFile(event) {
let el = event.target;
if (!el.files || !el.files.length) return;
importFile.value = el.files[0];
const reader = new FileReader();
reader.onload = (event) => importFileContent.value = event.target.result;
reader.readAsText(importFile.value);
}

function importAdversary() {
let newProfile = {};
let lastKey;
importFileContent.value.split("\n").forEach((line) => {
// Remove comments
line = line.split('#')[0];
// Line has a key-value pair
let keyValSplit = line.split(':');
if (keyValSplit.length >= 2) {
if (keyValSplit[1]) {
newProfile[keyValSplit[0]] = keyValSplit[1].trim();
} else {
lastKey = keyValSplit[0];
newProfile[lastKey] = [];
}
}
// Line is a list item
if (line.trim()[0] === '-' && line.trim() !== '---') {
newProfile[lastKey].push(line.replace('-', '').trim());
}
});

adversaryStore.createAdversary($api, newProfile);
importFile.value = {};
importFileContent.value = {};
modals.value.adversaries.showImport = false;
}
</script>

<template lang="pug">
.modal(:class="{ 'is-active': modals.adversaries.showImport }")
.modal-background(@click="modals.adversaries.showImport = false")
.modal-card
header.modal-card-head
p.modal-card-title Import Adversary
.modal-card-body
p.block Import an adversary in YAML format. Export any existing adversary to see the required format.
.file.has-name.is-fullwidth
label.file-label
input.file-input(accept=".yml,.yaml" type="file" @change="uploadImportFile")
span.file-cta
span.file-icon
font-awesome-icon(icon="fas fa-upload")
span.file-label Choose a file...
span.file-name {{ importFile ? importFile.name : '' }}
footer.modal-card-foot.is-flex.is-justify-content-flex-end
button.button(@click="modals.adversaries.showImport = false") Close
button.button.is-primary(@click="importAdversary()")
span.icon
font-awesome-icon(icon="fas fa-save")
span Import
</template>
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backup/temporary file committed into src/ (*.bkp). These increase repo noise and can confuse tooling. Please remove this file from the PR (and ideally add *.bkp to .gitignore if these are generated locally).

Suggested change
<script setup>
import { ref, inject } from "vue";
import { useCoreDisplayStore } from "../../stores/coreDisplayStore";
import { storeToRefs } from "pinia";
import { useAdversaryStore } from "../../stores/adversaryStore";
const $api = inject("$api");
const adversaryStore = useAdversaryStore();
const coreDisplayStore = useCoreDisplayStore();
const { modals } = storeToRefs(coreDisplayStore);
let importFile = ref({});
let importFileContent = ref({});
function uploadImportFile(event) {
let el = event.target;
if (!el.files || !el.files.length) return;
importFile.value = el.files[0];
const reader = new FileReader();
reader.onload = (event) => importFileContent.value = event.target.result;
reader.readAsText(importFile.value);
}
function importAdversary() {
let newProfile = {};
let lastKey;
importFileContent.value.split("\n").forEach((line) => {
// Remove comments
line = line.split('#')[0];
// Line has a key-value pair
let keyValSplit = line.split(':');
if (keyValSplit.length >= 2) {
if (keyValSplit[1]) {
newProfile[keyValSplit[0]] = keyValSplit[1].trim();
} else {
lastKey = keyValSplit[0];
newProfile[lastKey] = [];
}
}
// Line is a list item
if (line.trim()[0] === '-' && line.trim() !== '---') {
newProfile[lastKey].push(line.replace('-', '').trim());
}
});
adversaryStore.createAdversary($api, newProfile);
importFile.value = {};
importFileContent.value = {};
modals.value.adversaries.showImport = false;
}
</script>
<template lang="pug">
.modal(:class="{ 'is-active': modals.adversaries.showImport }")
.modal-background(@click="modals.adversaries.showImport = false")
.modal-card
header.modal-card-head
p.modal-card-title Import Adversary
.modal-card-body
p.block Import an adversary in YAML format. Export any existing adversary to see the required format.
.file.has-name.is-fullwidth
label.file-label
input.file-input(accept=".yml,.yaml" type="file" @change="uploadImportFile")
span.file-cta
span.file-icon
font-awesome-icon(icon="fas fa-upload")
span.file-label Choose a file...
span.file-name {{ importFile ? importFile.name : '' }}
footer.modal-card-foot.is-flex.is-justify-content-flex-end
button.button(@click="modals.adversaries.showImport = false") Close
button.button.is-primary(@click="importAdversary()")
span.icon
font-awesome-icon(icon="fas fa-save")
span Import
</template>

Copilot uses AI. Check for mistakes.
Comment thread src/main.js
Comment on lines 17 to +33
const app = createApp(App);
// Set default API url
const $api = axios.create({
withCredentials: true,
});
$api.interceptors.response.use(
(response) => response,
(error) => {
// connection lost (restart)
if (!error.response) {
const displayStore = useCoreDisplayStore(pinia);

// ignore connection loss during restart
if (displayStore?.restarting) {
return Promise.reject(error);
}

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pinia is referenced inside the axios interceptor (useCoreDisplayStore(pinia)), but no pinia instance exists in this file. This will throw at runtime. Create a const pinia = createPinia(); app.use(pinia); and reference that instance in the interceptor (or avoid passing an explicit pinia by calling the store after installation).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +31
<script setup>
import { ref, inject } from "vue";
import { useCoreDisplayStore } from "../../stores/coreDisplayStore";
import { storeToRefs } from "pinia";
import { useAdversaryStore } from "../../stores/adversaryStore";

const $api = inject("$api");

const adversaryStore = useAdversaryStore();
const coreDisplayStore = useCoreDisplayStore();
const { modals } = storeToRefs(coreDisplayStore);
</script>

<template lang="pug">
.modal(:class="{ 'is-active': modals.adversaries.showDeleteConfirm }")
.modal-background(@click="modals.adversaries.showDeleteConfirm = false")
.modal-card
header.modal-card-head
p.modal-card-title Delete Adversary
.modal-card-body
br
p.block Are you sure you want to delete this adversary? This action cannot be undone
br

footer.modal-card-foot.is-flex.is-justify-content-flex-end
button.button(@click="modals.adversaries.showDeleteConfirm = false") Cancel
button.button.is-danger(@click="adversaryStore.deleteAdversary($api) && (modals.adversaries.showDeleteConfirm = false)")
span.icon
font-awesome-icon(icon="fas fa-trash")
span Delete
</template> No newline at end of file
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backup/temporary file committed into src/ (*.bkp). These increase repo noise and can confuse tooling. Please remove this file from the PR (and ideally add *.bkp to .gitignore if these are generated locally).

Suggested change
<script setup>
import { ref, inject } from "vue";
import { useCoreDisplayStore } from "../../stores/coreDisplayStore";
import { storeToRefs } from "pinia";
import { useAdversaryStore } from "../../stores/adversaryStore";
const $api = inject("$api");
const adversaryStore = useAdversaryStore();
const coreDisplayStore = useCoreDisplayStore();
const { modals } = storeToRefs(coreDisplayStore);
</script>
<template lang="pug">
.modal(:class="{ 'is-active': modals.adversaries.showDeleteConfirm }")
.modal-background(@click="modals.adversaries.showDeleteConfirm = false")
.modal-card
header.modal-card-head
p.modal-card-title Delete Adversary
.modal-card-body
br
p.block Are you sure you want to delete this adversary? This action cannot be undone
br
footer.modal-card-foot.is-flex.is-justify-content-flex-end
button.button(@click="modals.adversaries.showDeleteConfirm = false") Cancel
button.button.is-danger(@click="adversaryStore.deleteAdversary($api) && (modals.adversaries.showDeleteConfirm = false)")
span.icon
font-awesome-icon(icon="fas fa-trash")
span Delete
</template>

Copilot uses AI. Check for mistakes.
Comment thread src/main.js
Comment on lines +39 to +40
if (error.response?.status === 401 &&
! error.config?.url?.includes("/api/v2/health")) {
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if (error.response?.status === 401 && ! error.config?.url?.includes(...)) block contains a stray ! on its own line/with extra whitespace, which makes this file invalid JS and will break the build. Please fix the condition formatting so it parses correctly.

Suggested change
if (error.response?.status === 401 &&
! error.config?.url?.includes("/api/v2/health")) {
if (
error.response?.status === 401 &&
!error.config?.url?.includes("/api/v2/health")
) {

Copilot uses AI. Check for mistakes.
Comment on lines +205 to 227
if (!abilityTemplate) {
console.warn(
`[Automation] Ability not found in abilityStore for step ${index}: ${stepId} — using step data`
);
// Fallback: use step itself as the ability

}

const clonedAbility = cloneDeep(abilityTemplate);
const stepUuid = stepObj.step_uuid || uuidv4();
stepObj.step_uuid = stepUuid;

// 1️⃣ Merge metadata (step overrides template)
const metadata = {
...(clonedAbility.metadata || {}),
...(stepObj.metadata || {}),
};
metadata.executor_facts ??= {};

// 2️⃣ 🔒 FIRST: establish stable executor keys
clonedAbility.executors.forEach(exec => {
exec.key = `${stepUuid}::${exec.name}::${exec.platform}`;
});
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateSelectedAdversaryAbilities logs a warning when abilityTemplate is not found, but then still does cloneDeep(abilityTemplate) and immediately dereferences clonedAbility.executors, which will throw. Add an explicit fallback return (e.g., return a normalized step object based on stepObj) when abilityTemplate is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +733 to +740
v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)"
v-tooltip="`This ability has requirements: (${abilityDependencies[ability.step_uuid].requireTypes})`"
)
span.icon.is-small
font-awesome-icon(icon="fas fa-lock")
td.has-text-centered(:class="{ 'lock': onHoverLocks.indexOf(ability.step_uuid) > -1 }")
span(
v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)"
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Requires" column is gated by getExecutorDetail('parser', ability), but it should be checking requirements (e.g., getExecutorDetail('requirements', ability)). As written, the lock icon/tooltip will be wrong for abilities that have requirements but no parsers (and vice versa).

Suggested change
v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)"
v-tooltip="`This ability has requirements: (${abilityDependencies[ability.step_uuid].requireTypes})`"
)
span.icon.is-small
font-awesome-icon(icon="fas fa-lock")
td.has-text-centered(:class="{ 'lock': onHoverLocks.indexOf(ability.step_uuid) > -1 }")
span(
v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)"
v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('requirements', ability)"
v-tooltip="`This ability has requirements: (${abilityDependencies[ability.step_uuid].requireTypes})`"
)
span.icon.is-small
font-awesome-icon(icon="fas fa-lock")
td.has-text-centered(:class="{ 'lock': onHoverLocks.indexOf(ability.step_uuid) > -1 }")
span(
v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('requirements', ability)"

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +51
if (!enabledPlugins.value) return []

return enabledPlugins.value.filter(p => {
const name = typeof p === "string" ? p : p.name
return name && name !== "magma"
})
})
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid automated semicolon insertion (90% of all statements in the enclosing script have an explicit semicolon).

Suggested change
if (!enabledPlugins.value) return []
return enabledPlugins.value.filter(p => {
const name = typeof p === "string" ? p : p.name
return name && name !== "magma"
})
})
if (!enabledPlugins.value) return [];
return enabledPlugins.value.filter(p => {
const name = typeof p === "string" ? p : p.name;
return name && name !== "magma";
});
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants