Merge remote-tracking branch 'origin/fix/copilot-credential-setup-ui' into combined-preview-test

This commit is contained in:
Zamil Majdy
2026-04-02 16:42:12 +02:00
5 changed files with 149 additions and 50 deletions

View File

@@ -48,27 +48,41 @@ logger = logging.getLogger(__name__)
def get_inputs_from_schema(
input_schema: dict[str, Any],
exclude_fields: set[str] | None = None,
input_data: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Extract input field info from JSON schema."""
"""Extract input field info from JSON schema.
When *input_data* is provided, each field's ``value`` key is populated
with the value the CoPilot already supplied — so the frontend can
prefill the form instead of showing empty inputs. Fields marked
``advanced`` in the schema are flagged so the frontend can hide them
by default (matching the builder behaviour).
"""
if not isinstance(input_schema, dict):
return []
exclude = exclude_fields or set()
properties = input_schema.get("properties", {})
required = set(input_schema.get("required", []))
provided = input_data or {}
return [
{
results: list[dict[str, Any]] = []
for name, schema in properties.items():
if name in exclude:
continue
entry: dict[str, Any] = {
"name": name,
"title": schema.get("title", name),
"type": schema.get("type", "string"),
"description": schema.get("description", ""),
"required": name in required,
"default": schema.get("default"),
"advanced": schema.get("advanced", False),
}
for name, schema in properties.items()
if name not in exclude
]
if name in provided:
entry["value"] = provided[name]
results.append(entry)
return results
async def execute_block(
@@ -446,7 +460,9 @@ async def prepare_block_for_execution(
requirements={
"credentials": missing_creds_list,
"inputs": get_inputs_from_schema(
input_schema, exclude_fields=credentials_fields
input_schema,
exclude_fields=credentials_fields,
input_data=input_data,
),
"execution_modes": ["immediate"],
},

View File

@@ -30,7 +30,7 @@ class VirusScanResult(BaseModel):
class VirusScannerSettings(BaseSettings):
# Tunables for the scanner layer (NOT the ClamAV daemon).
clamav_service_host: str = "localhost"
clamav_service_host: str = "clamav"
clamav_service_port: int = 3310
clamav_service_timeout: int = 60
clamav_service_enabled: bool = True

View File

@@ -98,6 +98,7 @@ services:
- CLAMD_CONF_MaxScanSize=100M
- CLAMD_CONF_MaxThreads=12
- CLAMD_CONF_ReadTimeout=300
- CLAMD_CONF_TCPAddr=0.0.0.0
healthcheck:
test: ["CMD-SHELL", "clamdscan --version || exit 1"]
interval: 30s

View File

@@ -6,25 +6,21 @@ import { Text } from "@/components/atoms/Text/Text";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
import {
buildExpectedInputsSchema,
buildSiblingInputsFromCredentials,
coerceCredentialFields,
coerceExpectedInputs,
extractInitialValues,
} from "./helpers";
interface Props {
output: SetupRequirementsResponse;
/** Override the message sent to the chat when the user clicks Proceed after connecting credentials.
* Defaults to "Please re-run this step now." */
retryInstruction?: string;
/** Override the label shown above the credentials section.
* Defaults to "Credentials". */
credentialsLabel?: string;
/** Called after Proceed is clicked so the parent can persist the dismissed state
* across remounts (avoids re-enabling the Proceed button on remount). */
onComplete?: () => void;
}
@@ -39,8 +35,8 @@ export function SetupRequirementsCard({
const [inputCredentials, setInputCredentials] = useState<
Record<string, CredentialsMetaInput | undefined>
>({});
const [inputValues, setInputValues] = useState<Record<string, unknown>>({});
const [hasSent, setHasSent] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
const { credentialFields, requiredCredentials } = coerceCredentialFields(
output.setup_info.user_readiness?.missing_credentials,
@@ -50,7 +46,27 @@ export function SetupRequirementsCard({
(output.setup_info.requirements as Record<string, unknown>)?.inputs,
);
const inputSchema = buildExpectedInputsSchema(expectedInputs);
const initialValues = useMemo(
() => extractInitialValues(expectedInputs),
[expectedInputs],
);
const [inputValues, setInputValues] =
useState<Record<string, unknown>>(initialValues);
const hasAdvancedFields = expectedInputs.some((i) => i.advanced);
const inputSchema = buildExpectedInputsSchema(expectedInputs, showAdvanced);
// Build siblingInputs for credential modal host prefill.
// Prefer discriminator_values from the credential response, but also
// include values from input_data (e.g. url field) so the host pattern
// can be extracted even when discriminator_values is empty.
const siblingInputs = useMemo(() => {
const fromCreds = buildSiblingInputsFromCredentials(
output.setup_info.user_readiness?.missing_credentials,
);
return { ...inputValues, ...fromCreds };
}, [output.setup_info.user_readiness?.missing_credentials, inputValues]);
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
setInputCredentials((prev) => ({ ...prev, [key]: value }));
@@ -63,10 +79,10 @@ export function SetupRequirementsCard({
const needsInputs = inputSchema !== null;
const requiredInputNames = expectedInputs
.filter((i) => i.required)
.filter((i) => i.required && !i.advanced)
.map((i) => i.name);
const isAllInputsComplete =
needsInputs &&
!needsInputs ||
requiredInputNames.every((name) => {
const v = inputValues[name];
return v !== undefined && v !== null && v !== "";
@@ -77,8 +93,7 @@ export function SetupRequirementsCard({
}
const canRun =
(!needsCredentials || isAllCredentialsComplete) &&
(!needsInputs || isAllInputsComplete);
(!needsCredentials || isAllCredentialsComplete) && isAllInputsComplete;
function handleRun() {
setHasSent(true);
@@ -118,7 +133,7 @@ export function SetupRequirementsCard({
credentialFields={credentialFields}
requiredCredentials={requiredCredentials}
inputCredentials={inputCredentials}
inputValues={{}}
inputValues={siblingInputs}
onCredentialChange={handleCredentialChange}
/>
</div>
@@ -133,7 +148,9 @@ export function SetupRequirementsCard({
<FormRenderer
jsonSchema={inputSchema}
className="mb-3 mt-3"
handleChange={(v) => setInputValues(v.formData ?? {})}
handleChange={(v) =>
setInputValues((prev) => ({ ...prev, ...(v.formData ?? {}) }))
}
uiSchema={{
"ui:submitButtonOptions": { norender: true },
}}
@@ -143,6 +160,15 @@ export function SetupRequirementsCard({
size: "small",
}}
/>
{hasAdvancedFields && (
<button
type="button"
className="text-xs text-muted-foreground underline"
onClick={() => setShowAdvanced((v) => !v)}
>
{showAdvanced ? "Hide advanced fields" : "Show advanced fields"}
</button>
)}
</div>
)}

View File

@@ -71,21 +71,58 @@ export function coerceCredentialFields(rawMissingCredentials: unknown): {
return { credentialFields, requiredCredentials };
}
export function coerceExpectedInputs(rawInputs: unknown): Array<{
/**
* Build a sibling-inputs dict from the missing_credentials discriminator values.
*
* When the backend resolves credentials for host-scoped blocks (e.g.
* SendAuthenticatedWebRequestBlock), it adds the target URL to
* `discriminator_values`. The credential modal uses `siblingInputs`
* to extract the host and prefill the "Host Pattern" field.
*
* This function builds that mapping from the `discriminator` field name
* and the first `discriminator_values` entry for each credential.
*/
export function buildSiblingInputsFromCredentials(
rawMissingCredentials: unknown,
): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (!rawMissingCredentials || typeof rawMissingCredentials !== "object")
return result;
const missing = rawMissingCredentials as Record<string, unknown>;
for (const value of Object.values(missing)) {
if (!value || typeof value !== "object") continue;
const cred = value as Record<string, unknown>;
const discriminator =
typeof cred.discriminator === "string" ? cred.discriminator : null;
const discriminatorValues = Array.isArray(cred.discriminator_values)
? cred.discriminator_values.filter(
(v): v is string => typeof v === "string",
)
: [];
if (discriminator && discriminatorValues.length > 0) {
result[discriminator] = discriminatorValues[0];
}
}
return result;
}
interface ExpectedInput {
name: string;
title: string;
type: string;
description?: string;
required: boolean;
}> {
advanced: boolean;
value?: unknown;
}
export function coerceExpectedInputs(rawInputs: unknown): ExpectedInput[] {
if (!Array.isArray(rawInputs)) return [];
const results: Array<{
name: string;
title: string;
type: string;
description?: string;
required: boolean;
}> = [];
const results: ExpectedInput[] = [];
rawInputs.forEach((value, index) => {
if (!value || typeof value !== "object") return;
@@ -105,15 +142,13 @@ export function coerceExpectedInputs(rawInputs: unknown): Array<{
? input.description.trim()
: undefined;
const required = Boolean(input.required);
const advanced = Boolean(input.advanced);
const item: {
name: string;
title: string;
type: string;
description?: string;
required: boolean;
} = { name, title, type, required };
const item: ExpectedInput = { name, title, type, required, advanced };
if (description) item.description = description;
if (input.value !== undefined && input.value !== null) {
item.value = input.value;
}
results.push(item);
});
@@ -123,17 +158,20 @@ export function coerceExpectedInputs(rawInputs: unknown): Array<{
/**
* Build an RJSF schema from expected inputs so they can be rendered
* as a dynamic form via FormRenderer.
*
* When ``showAdvanced`` is false (default), fields marked ``advanced``
* are excluded — matching the builder behaviour where advanced fields
* are hidden behind a toggle.
*/
export function buildExpectedInputsSchema(
expectedInputs: Array<{
name: string;
title: string;
type: string;
description?: string;
required: boolean;
}>,
expectedInputs: ExpectedInput[],
showAdvanced = false,
): RJSFSchema | null {
if (expectedInputs.length === 0) return null;
const visible = showAdvanced
? expectedInputs
: expectedInputs.filter((i) => !i.advanced);
if (visible.length === 0) return null;
const TYPE_MAP: Record<string, string> = {
string: "string",
@@ -150,12 +188,14 @@ export function buildExpectedInputsSchema(
const properties: Record<string, Record<string, unknown>> = {};
const required: string[] = [];
for (const input of expectedInputs) {
properties[input.name] = {
for (const input of visible) {
const prop: Record<string, unknown> = {
type: TYPE_MAP[input.type.toLowerCase()] ?? "string",
title: input.title,
...(input.description ? { description: input.description } : {}),
};
if (input.description) prop.description = input.description;
if (input.value !== undefined) prop.default = input.value;
properties[input.name] = prop;
if (input.required) required.push(input.name);
}
@@ -165,3 +205,19 @@ export function buildExpectedInputsSchema(
...(required.length > 0 ? { required } : {}),
};
}
/**
* Extract initial form values from expected inputs that have a
* prefilled ``value`` from the backend.
*/
export function extractInitialValues(
expectedInputs: ExpectedInput[],
): Record<string, unknown> {
const values: Record<string, unknown> = {};
for (const input of expectedInputs) {
if (input.value !== undefined && input.value !== null) {
values[input.name] = input.value;
}
}
return values;
}