Skip to content

Fix review feedback: Proxmox API safety, type deduplication, accessibility, and code cleanup#35

Merged
alvagante merged 2 commits into090from
copilot/sub-pr-33
Mar 14, 2026
Merged

Fix review feedback: Proxmox API safety, type deduplication, accessibility, and code cleanup#35
alvagante merged 2 commits into090from
copilot/sub-pr-33

Conversation

Copy link
Contributor

Copilot AI commented Mar 14, 2026

Several issues flagged in review: incorrect plugin interface casting in node lifecycle/destroy routes, process-wide TLS mutation, hardcoded provisioning capabilities, duplicated types, stale dead code, and accessibility regressions.

Backend — API Safety

  • inventory.ts: executeAction now retrieves getExecutionTool("proxmox") and passes a correctly-typed Action ({ type: "task", target, action }). Destroy route uses executeAction({ action: "destroy_vm", parameters: { node, vmid } }) — no more unsafe casts to InformationSourcePlugin.
  • inventory.ts: Added Number.isFinite(vmid) guard; parseInt on a malformed node ID silently produced NaN before.
  • streaming.ts: Query-param bearer token only promoted to Authorization header when no header is already present; token deleted from req.query to reduce log leakage.
  • ProxmoxClient.ts: Removed process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' (process-wide, affects all HTTPS). Replaced with a warning log directing operators to configure a CA cert; per-client TLS bypass noted as future work.

Backend — Types & Code Quality

  • proxmox/types.ts: Removed duplicate ProvisioningCapability; re-exports the shared definition from integrations/types.ts. Added optional validation field to shared CapabilityParameter.
  • provisioning.ts: Calls proxmox.listProvisioningCapabilities() instead of a hardcoded inline list that would silently drift.
  • IntegrationManager.ts: Collapsed four nested/duplicate JSDoc blocks on deduplicateNodes to one; removed dead nodesMatch and extractNodeIdentifiers helpers (superseded by NodeLinkingService).
  • NodeLinkingService.ts: Removed unused nodeSource variable. Fixed uri field: was joining all source URIs with ", " into a single string — now keeps the primary URI; per-source URIs are already in sourceData.

Frontend — Accessibility

  • GroupActionModal.svelte: Restored tabindex="0" on the scrollable role="region".
  • ManageTab.svelte: Overlay tabindex="-1"tabindex="0". Escape-to-close moved from an onkeydown on the unfocusable backdrop to a $effect window-level listener (active only while dialog is open).
  • ExecuteCommandForm.svelte: Tool selector changed from <div role="group" aria-label> to <fieldset><legend> for proper semantic labeling.
  • AnsiblePlaybookInterface.svelte: Decorative SVG marked aria-hidden="true" focusable="false".

Tests & Docs

  • Fixed IntegrationManager test that asserted .source (singular) on LinkedNode — that property is never set; updated to assert sourceData entries and sources array.
  • Corrected Proxmox priority comment in server.ts (was "Puppetserver (8)"; actual scheme is Bolt/PuppetDB=10, Proxmox=7, Hiera=6).
  • Fixed typo: catchedcaught in security-best-practices.md.

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

… and code quality improvements

Co-authored-by: alvagante <283804+alvagante@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 14, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • httpstat.us
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --conditions node --conditions development --experimental-import-meta-resolve --require /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/suppress-warnings.cjs /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/dist/workers/forks.js l/config p/bin/git l/openssl/include --no�� lude e 14.0/src (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --conditions node --conditions development --experimental-import-meta-resolve --require /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/suppress-warnings.cjs /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/dist/workers/forks.js 14.0/deps/v8/inc--norc nings.cjs /forks.js /hom�� 14.0/include/node --real_openssl_major=3 n/node get 14.0/deps/openss--norc bash 14.0/deps/openssl/openssl/include (dns block)
  • puppetdb.example.com
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --conditions node --conditions development --experimental-import-meta-resolve --require /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/suppress-warnings.cjs /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/dist/workers/forks.js lude github.com python3 -c e (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --conditions node --conditions development --experimental-import-meta-resolve --require /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/suppress-warnings.cjs /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/dist/workers/forks.js l/config de/node/bin/git l/openssl/includRelease/obj.target/sshcrypto/src/binding.o -e s/src/impl_x86_windows.o 14.0/include/node 3 14.0/src lude 14.0/deps/openssnode sh (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.14.0/x64/bin/node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --conditions node --conditions development --experimental-import-meta-resolve --require /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/suppress-warnings.cjs /home/REDACTED/work/pabawi/pabawi/node_modules/vitest/dist/workers/forks.js 14.0/deps/v8/inc--norc nings.cjs /forks.js /hom�� 14.0/include/node --real_openssl_major=3 n/vitest /impl_aarch64_libash 14.0/deps/openss--norc (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] [090] Add Proxmox provisioning and lifecycle management support Fix review feedback: Proxmox API safety, type deduplication, accessibility, and code cleanup Mar 14, 2026
Copilot AI requested a review from alvagante March 14, 2026 16:20
@alvagante alvagante marked this pull request as ready for review March 14, 2026 18:50
Copilot AI review requested due to automatic review settings March 14, 2026 18:50
@alvagante alvagante merged commit e4e2335 into 090 Mar 14, 2026
1 check passed
Copy link
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

Addresses prior review feedback by tightening Proxmox integration safety (plugin interface usage + TLS handling), deduplicating shared types, cleaning up inventory/linking code, and restoring several frontend accessibility semantics.

Changes:

  • Backend: route-level Proxmox execution now uses properly-shaped Action objects and avoids unsafe interface casts; SSE token handling reduces query-param leakage.
  • Backend: capability/type deduplication and provisioning capabilities now sourced from the Proxmox integration; node linking/aggregation cleanup.
  • Frontend: restores/improves keyboard and semantic accessibility for dialogs, regions, and form labeling.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
package-lock.json Bumps lockfile workspace versions to 0.9.0.
frontend/src/components/ManageTab.svelte Confirmation dialog keyboard handling adjusted (Escape listener + backdrop tabindex).
frontend/src/components/GroupActionModal.svelte Restores focusability of scrollable region for keyboard navigation.
frontend/src/components/ExecuteCommandForm.svelte Improves semantic labeling via fieldset/legend.
frontend/src/components/AnsiblePlaybookInterface.svelte Marks decorative SVG as hidden from assistive tech.
backend/test/integrations/IntegrationManager.test.ts Updates assertions to validate sourceData/sources instead of non-existent .source.
backend/src/server.ts Fixes Proxmox priority comment to match actual priority scheme.
backend/src/routes/streaming.ts Only promotes query token to Authorization when header absent; deletes query token afterward.
backend/src/routes/inventory.ts Uses getExecutionTool("proxmox") and correct Action shapes; adds vmid finite-number guard.
backend/src/routes/integrations/provisioning.ts Removes duplicated capability types; returns Proxmox capabilities from integration method.
backend/src/integrations/types.ts Adds optional validation to CapabilityParameter.
backend/src/integrations/proxmox/types.ts Removes duplicate ProvisioningCapability and re-exports shared type.
backend/src/integrations/proxmox/ProxmoxClient.ts Removes process-wide TLS disable; logs operator guidance instead.
backend/src/integrations/NodeLinkingService.ts Cleans linking logic (primary URI selection, unused var removal).
backend/src/integrations/IntegrationManager.ts Collapses duplicate JSDoc and removes dead helpers in favor of NodeLinkingService.
.kiro/steering/security-best-practices.md Fixes typo/wording around allowlisting secrets.

Comment on lines +48 to 62
// Configure SSL behavior
// NOTE: Setting NODE_TLS_REJECT_UNAUTHORIZED is process-wide and would disable TLS verification
// for ALL HTTPS traffic, not just Proxmox. Instead, log a warning to guide the operator.
// Per-client TLS bypass via a custom HTTPS agent/dispatcher is the correct solution.
if (config.ssl && config.ssl.rejectUnauthorized === false) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
this.logger.debug("Disabled TLS certificate verification for Proxmox", {
component: "ProxmoxClient",
operation: "constructor",
});
this.logger.warn(
"Proxmox ssl.rejectUnauthorized=false is set, but per-client TLS bypass is not yet implemented. " +
"Proxmox connections will use the default TLS verification. " +
"Configure a trusted CA certificate (ssl.ca) to connect to Proxmox with self-signed certs.",
{
component: "ProxmoxClient",
operation: "constructor",
}
);
}
Comment on lines +915 to +928
/**
* Deduplicate and link nodes by matching identifiers.
*
* @param nodes - Array of nodes potentially with duplicates
* @returns Deduplicated array of nodes
* When multiple sources provide the same node (matched by identifiers like certname,
* hostname, or URI), merge them into a single node entry with all sources tracked.
* The node data is taken from the highest priority source, but all sources and URIs
* are recorded in sourceData.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
/**
* Deduplicate and link nodes by ID
*
* When multiple sources provide the same node (by ID), merge them into a single
* node entry with all sources tracked. This matches the behavior of group linking.
* The node data is taken from the highest priority source, but all sources are
* recorded in the sources array.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
/**
* Deduplicate and link nodes by ID
*
* When multiple sources provide the same node (by ID), merge them into a single
* node entry with all sources tracked. This matches the behavior of group linking.
* The node data is taken from the highest priority source, but all sources are
* recorded in the sources array.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
/**
* Deduplicate and link nodes by matching identifiers
*
* When multiple sources provide the same node (matched by identifiers like certname,
* hostname, URI), merge them into a single node entry with all sources tracked.
* This matches the behavior of group linking. The node data is taken from the highest
* priority source, but all sources and URIs are recorded.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
private deduplicateNodes(nodes: Node[]): LinkedNode[] {
// Use NodeLinkingService to link nodes by identifiers
// This already handles source-specific data correctly
return this.nodeLinkingService.linkNodes(nodes);
}

/**
* Check if a node matches a linked node by comparing identifiers
*
* @param node - Node to check
* @param linkedNode - Linked node to match against
* @returns True if nodes match
* @private
*/
private nodesMatch(node: Node, linkedNode: LinkedNode): boolean {
const nodeIdentifiers = this.extractNodeIdentifiers(node);
const linkedIdentifiers = this.extractNodeIdentifiers(linkedNode);

// Check if any identifiers match
for (const id of nodeIdentifiers) {
if (linkedIdentifiers.includes(id)) {
return true;
}
}

return false;
}

/**
* Extract all possible identifiers from a node
*
* @param node - Node to extract identifiers from
* @returns Array of identifiers (normalized to lowercase)
* @private
*/
private extractNodeIdentifiers(node: Node): string[] {
const identifiers: string[] = [];

// Add node ID
if (node.id) {
identifiers.push(node.id.toLowerCase());
}

// Add node name (certname)
// Skip empty names to prevent incorrect linking
if (node.name && node.name.trim() !== "") {
// Normalize hostname: trim, lowercase, remove domain suffix
const normalizedName = node.name.trim().toLowerCase();
identifiers.push(normalizedName);

// Also add short hostname (without domain)
const shortName = normalizedName.split('.')[0];
if (shortName !== normalizedName) {
identifiers.push(shortName);
}
}

// Add URI hostname (extract from URI)
// Skip Proxmox URIs as they use format proxmox://node/vmid where 'node' is not unique per VM
if (node.uri && !node.uri.startsWith("proxmox://")) {
try {
// Extract hostname from URI
// URIs can be in formats like:
// - ssh://hostname
// - hostname
// - hostname:port
const uriParts = node.uri.split("://");
const hostPart = uriParts.length > 1 ? uriParts[1] : uriParts[0];
const hostname = hostPart.split(":")[0].split("/")[0].trim().toLowerCase();

if (hostname) {
identifiers.push(hostname);

// Also add short hostname
const shortHostname = hostname.split('.')[0];
if (shortHostname !== hostname) {
identifiers.push(shortHostname);
}
}
} catch {
// Ignore URI parsing errors
}
}

// Add hostname from config if available
const nodeConfig = node.config as { hostname?: string } | undefined;
if (nodeConfig?.hostname) {
const normalizedHostname = nodeConfig.hostname.trim().toLowerCase();
identifiers.push(normalizedHostname);

// Also add short hostname
const shortHostname = normalizedHostname.split('.')[0];
if (shortHostname !== normalizedHostname) {
identifiers.push(shortHostname);
}
}

// Remove duplicates
return Array.from(new Set(identifiers));
}
private deduplicateNodes(nodes: Node[]): LinkedNode[] {
return this.nodeLinkingService.linkNodes(nodes);
}
Comment on lines +915 to +928
/**
* Deduplicate and link nodes by matching identifiers.
*
* @param nodes - Array of nodes potentially with duplicates
* @returns Deduplicated array of nodes
* When multiple sources provide the same node (matched by identifiers like certname,
* hostname, or URI), merge them into a single node entry with all sources tracked.
* The node data is taken from the highest priority source, but all sources and URIs
* are recorded in sourceData.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
/**
* Deduplicate and link nodes by ID
*
* When multiple sources provide the same node (by ID), merge them into a single
* node entry with all sources tracked. This matches the behavior of group linking.
* The node data is taken from the highest priority source, but all sources are
* recorded in the sources array.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
/**
* Deduplicate and link nodes by ID
*
* When multiple sources provide the same node (by ID), merge them into a single
* node entry with all sources tracked. This matches the behavior of group linking.
* The node data is taken from the highest priority source, but all sources are
* recorded in the sources array.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
/**
* Deduplicate and link nodes by matching identifiers
*
* When multiple sources provide the same node (matched by identifiers like certname,
* hostname, URI), merge them into a single node entry with all sources tracked.
* This matches the behavior of group linking. The node data is taken from the highest
* priority source, but all sources and URIs are recorded.
*
* @param nodes - Array of nodes from all sources
* @returns Deduplicated and linked array of nodes with source attribution
*/
private deduplicateNodes(nodes: Node[]): LinkedNode[] {
// Use NodeLinkingService to link nodes by identifiers
// This already handles source-specific data correctly
return this.nodeLinkingService.linkNodes(nodes);
}

/**
* Check if a node matches a linked node by comparing identifiers
*
* @param node - Node to check
* @param linkedNode - Linked node to match against
* @returns True if nodes match
* @private
*/
private nodesMatch(node: Node, linkedNode: LinkedNode): boolean {
const nodeIdentifiers = this.extractNodeIdentifiers(node);
const linkedIdentifiers = this.extractNodeIdentifiers(linkedNode);

// Check if any identifiers match
for (const id of nodeIdentifiers) {
if (linkedIdentifiers.includes(id)) {
return true;
}
}

return false;
}

/**
* Extract all possible identifiers from a node
*
* @param node - Node to extract identifiers from
* @returns Array of identifiers (normalized to lowercase)
* @private
*/
private extractNodeIdentifiers(node: Node): string[] {
const identifiers: string[] = [];

// Add node ID
if (node.id) {
identifiers.push(node.id.toLowerCase());
}

// Add node name (certname)
// Skip empty names to prevent incorrect linking
if (node.name && node.name.trim() !== "") {
// Normalize hostname: trim, lowercase, remove domain suffix
const normalizedName = node.name.trim().toLowerCase();
identifiers.push(normalizedName);

// Also add short hostname (without domain)
const shortName = normalizedName.split('.')[0];
if (shortName !== normalizedName) {
identifiers.push(shortName);
}
}

// Add URI hostname (extract from URI)
// Skip Proxmox URIs as they use format proxmox://node/vmid where 'node' is not unique per VM
if (node.uri && !node.uri.startsWith("proxmox://")) {
try {
// Extract hostname from URI
// URIs can be in formats like:
// - ssh://hostname
// - hostname
// - hostname:port
const uriParts = node.uri.split("://");
const hostPart = uriParts.length > 1 ? uriParts[1] : uriParts[0];
const hostname = hostPart.split(":")[0].split("/")[0].trim().toLowerCase();

if (hostname) {
identifiers.push(hostname);

// Also add short hostname
const shortHostname = hostname.split('.')[0];
if (shortHostname !== hostname) {
identifiers.push(shortHostname);
}
}
} catch {
// Ignore URI parsing errors
}
}

// Add hostname from config if available
const nodeConfig = node.config as { hostname?: string } | undefined;
if (nodeConfig?.hostname) {
const normalizedHostname = nodeConfig.hostname.trim().toLowerCase();
identifiers.push(normalizedHostname);

// Also add short hostname
const shortHostname = normalizedHostname.split('.')[0];
if (shortHostname !== normalizedHostname) {
identifiers.push(shortHostname);
}
}

// Remove duplicates
return Array.from(new Set(identifiers));
}
private deduplicateNodes(nodes: Node[]): LinkedNode[] {
return this.nodeLinkingService.linkNodes(nodes);
}
Comment on lines 333 to 339
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity dark:bg-gray-900 dark:bg-opacity-75"
onclick={cancelConfirmation}
onkeydown={(e) => e.key === 'Escape' && cancelConfirmation()}
role="button"
tabindex="-1"
tabindex="0"
aria-label="Close dialog"
></div>
Comment on lines 1181 to 1199
@@ -1190,14 +1191,11 @@ export function createInventoryRouter(
return;
}

// Execute the action
const proxmoxService = proxmoxSource as unknown as {
executeAction: (action: { action: string; target: string }) => Promise<unknown>;
};

const result = await proxmoxService.executeAction({
action: body.action,
// Execute the action with a properly-shaped Action object
const result = await proxmoxTool.executeAction({
type: "task",
target: nodeId,
action: body.action,
});
Comment on lines 1310 to +1357
@@ -1339,12 +1337,24 @@ export function createInventoryRouter(
const node = parts[1];
const vmid = parseInt(parts[2], 10);

// Execute the destroy action
const proxmoxService = proxmoxSource as unknown as {
destroyGuest: (node: string, vmid: number) => Promise<unknown>;
};
if (!Number.isFinite(vmid)) {
const errorResponse = {
error: {
code: "INVALID_NODE_ID",
message: "Invalid Proxmox node ID: vmid is not a valid number",
},
};

res.status(400).json(errorResponse);
return;
}

const result = await proxmoxService.destroyGuest(node, vmid);
const result = await proxmoxTool.executeAction({
type: "task",
target: nodeId,
action: "destroy_vm",
parameters: { node, vmid },
});
Comment on lines 45 to 69
// Check Proxmox integration
const proxmox = integrationManager.getExecutionTool("proxmox") as ProxmoxIntegration | null;

if (proxmox) {
// Determine integration status based on health check
let status: 'connected' | 'degraded' | 'not_configured' = 'not_configured';
const healthCheck = proxmox.getLastHealthCheck();

if (healthCheck) {
if (healthCheck.healthy) {
status = 'connected';
} else if (healthCheck.message?.includes('not initialized') || healthCheck.message?.includes('disabled')) {
status = 'not_configured';
} else {
status = 'degraded';
}
}

const proxmoxIntegration: ProvisioningIntegration = {
name: "proxmox",
displayName: "Proxmox VE",
type: "virtualization",
status,
capabilities: [
{
name: "create_vm",
description: "Create a new virtual machine",
operation: "create",
parameters: [
{ name: "vmid", type: "number", required: true, description: "Unique VM identifier", validation: { min: 100, max: 999999999 } },
{ name: "name", type: "string", required: true, description: "VM name", validation: { max: 50 } },
{ name: "node", type: "string", required: true, description: "Proxmox node name", validation: { max: 20 } },
{ name: "cores", type: "number", required: false, description: "Number of CPU cores", validation: { min: 1, max: 128 } },
{ name: "memory", type: "number", required: false, description: "Memory in MB", validation: { min: 16 } },
{ name: "sockets", type: "number", required: false, description: "Number of CPU sockets", validation: { min: 1, max: 4 } },
{ name: "cpu", type: "string", required: false, description: "CPU type" },
{ name: "scsi0", type: "string", required: false, description: "SCSI disk configuration" },
{ name: "ide2", type: "string", required: false, description: "IDE device configuration" },
{ name: "net0", type: "string", required: false, description: "Network interface configuration" },
{ name: "ostype", type: "string", required: false, description: "Operating system type" },
],
},
{
name: "create_lxc",
description: "Create a new LXC container",
operation: "create",
parameters: [
{ name: "vmid", type: "number", required: true, description: "Unique container identifier", validation: { min: 100, max: 999999999 } },
{ name: "hostname", type: "string", required: true, description: "Container hostname", validation: { max: 50 } },
{ name: "node", type: "string", required: true, description: "Proxmox node name", validation: { max: 20 } },
{ name: "ostemplate", type: "string", required: true, description: "OS template name" },
{ name: "cores", type: "number", required: false, description: "Number of CPU cores", validation: { min: 1, max: 128 } },
{ name: "memory", type: "number", required: false, description: "Memory in MB", validation: { min: 16 } },
{ name: "rootfs", type: "string", required: false, description: "Root filesystem configuration" },
{ name: "net0", type: "string", required: false, description: "Network interface configuration" },
{ name: "password", type: "string", required: false, description: "Root password" },
],
},
],
capabilities: proxmox.listProvisioningCapabilities(),
};
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.

3 participants