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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.259"
VERSION = "0.261.260"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
50 changes: 10 additions & 40 deletions application/v2_ui/src/components/admin/ConnectionTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
// out is to try them. Without this, configuring a connection means saving blind and
// waiting for a user to hit the failure.
//
// The test runs against the values currently on screen, not the saved ones, so a
// connection can be verified before it is committed. The one value the browser cannot
// supply is a stored credential: it holds only the redaction placeholder, and the server
// swaps that back for the real secret before making the call.
// The test runs against the values currently on screen, unsaved edits included, so a
// connection can be verified before it is committed. A saved value is read from wherever
// its field declares it is stored, which is not always a top-level key. The one value the
// browser cannot supply is a stored credential: it holds only the redaction placeholder,

Check warning on line 11 in application/v2_ui/src/components/admin/ConnectionTest.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 11 in application/v2_ui/src/components/admin/ConnectionTest.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
// and the server swaps that back for the real secret before making the call.

Check warning on line 12 in application/v2_ui/src/components/admin/ConnectionTest.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
//
// Nothing here knows about a specific service. Which test to run and how to shape its
// payload are both read from the field definition, so a new connection test is a schema
Expand All @@ -18,7 +19,7 @@
import { clsx } from 'clsx';
import { CheckCircle2, Loader2, PlugZap, XCircle } from 'lucide-react';
import { ApiError, api } from '../../lib/apiClient';
import { evaluateDependency, type AdminField } from '../../lib/adminFields';
import { buildConnectionTestPayload, type AdminField } from '../../lib/adminFields';
import type { Json } from '../../lib/types';

/** What the shared connection-test dispatcher returns. Shapes vary by test. */
Expand All @@ -37,27 +38,6 @@
guidance: string[];
}

/**
* Write a value at a dotted path, creating intermediate objects.
*
* Payload shapes are nested -- `direct.endpoint`, `apim.subscription_key` -- because that
* is what the existing test handlers expect. Declaring them flat and expanding here keeps
* the schema readable.
*/
function assignPath(target: Json, path: string, value: unknown): void {
const parts = path.split('.');
let cursor = target as Record<string, unknown>;

for (const part of parts.slice(0, -1)) {
if (typeof cursor[part] !== 'object' || cursor[part] === null) {
cursor[part] = {};
}
cursor = cursor[part] as Record<string, unknown>;
}

cursor[parts[parts.length - 1]] = value;
}

/**
* Turn a response into something worth showing.
*
Expand Down Expand Up @@ -85,19 +65,19 @@
field,
settings,
draft,
fieldsByKey,
disabled,
}: {
field: AdminField;
settings: Json;
draft: Json;
/** Every declared field by key, so a value saved at a nested path is found. */
fieldsByKey: Map<string, AdminField>;
disabled?: boolean;
}) {
const [running, setRunning] = useState(false);
const [outcome, setOutcome] = useState<TestOutcome | null>(null);

const read = (key: string): unknown =>
Object.prototype.hasOwnProperty.call(draft, key) ? draft[key] : settings[key];

const run = async () => {
if (!field.test_type) {
return;
Expand All @@ -106,17 +86,7 @@
setRunning(true);
setOutcome(null);

const payload: Json = { test_type: field.test_type };
for (const [path, source] of Object.entries(field.test_payload ?? {})) {
// A payload entry can be conditional, which is how one declaration covers
// both sides of an APIM-or-direct choice without sending the branch that is
// not in use.
if (!evaluateDependency(source.when, read)) {
continue;
}
const value = source.key !== undefined ? read(source.key) : source.value;
assignPath(payload, path, value ?? '');
}
const payload = buildConnectionTestPayload(field, settings, draft, fieldsByKey);

try {
const response = await api.post<ConnectionTestResponse>(
Expand Down
26 changes: 19 additions & 7 deletions application/v2_ui/src/components/admin/SettingsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@
* settings while a search narrows `fields` to the matches.
*/
hierarchyFields?: AdminField[];
/**
* Every declared field by key.
*
* A field saved at a nested path is found only through its declaration, so without
* this a gate stored that way -- the Web Search authentication type, say -- reads as

Check warning on line 109 in application/v2_ui/src/components/admin/SettingsSection.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
* unset and hides the fields that depend on it.
*/
fieldsByKey?: Map<string, AdminField>;
/** Force every group open, used while a search is filtering the page. */
forceExpanded?: boolean;
/** Opt-in presentation overrides; never changes the schema's behavior. */
Expand Down Expand Up @@ -283,6 +291,7 @@
status: statusProp,
icon,
hierarchyFields,
fieldsByKey,
renderField,
renderCapability,
forceExpanded,
Expand All @@ -297,8 +306,8 @@
);

const derivedStatus = useMemo(
() => computeSectionStatus(fields, settings, draft, statusRule),
[statusRule, fields, settings, draft],
() => computeSectionStatus(fields, settings, draft, statusRule, fieldsByKey),
[statusRule, fields, settings, draft, fieldsByKey],
);
const status = statusProp ?? derivedStatus;

Expand All @@ -308,16 +317,19 @@
);

const capabilityOn = capability?.key
? asBoolean(readSectionValue(settings, draft, capability.key))
? asBoolean(readSectionValue(settings, draft, capability.key, fieldsByKey))
: true;

// A section states each distinct prerequisite once, at the top, rather than repeating
// it on every field that carries it.
const requirements = useMemo(() => collectRequirements(fields, settings, draft), [fields, settings, draft]);
const requirements = useMemo(
() => collectRequirements(fields, settings, draft, fieldsByKey),
[fields, settings, draft, fieldsByKey],
);

const groups = useMemo(
() => groupFields(bodyFields.filter((field) => isFieldVisible(field, settings, draft))),
[bodyFields, settings, draft],
() => groupFields(bodyFields.filter((field) => isFieldVisible(field, settings, draft, fieldsByKey))),
[bodyFields, settings, draft, fieldsByKey],
);

const presentation = presentSectionStatus(status);
Expand Down Expand Up @@ -432,7 +444,7 @@
<RequirementNotice
key={requirement.key}
requirement={requirement}
satisfied={asBoolean(readSectionValue(settings, draft, requirement.key))}
satisfied={asBoolean(readSectionValue(settings, draft, requirement.key, fieldsByKey))}
/>
))}

Expand Down
109 changes: 89 additions & 20 deletions application/v2_ui/src/lib/adminFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,17 +419,7 @@
return draft[field.key];
}

// Two descriptors name a nested home: `settings_path` as segments, `paths` as
// dotted strings. The draft is always keyed flat, because that is what the PATCH
// sends and what the server unpacks; only the stored read follows the path.
let stored: unknown;
if (field.settings_path) {
stored = readNestedValue(settings, field.settings_path);
} else if (field.paths?.length) {
stored = readNestedSetting(settings, field.paths[0]);
} else {
stored = settings[field.key];
}
const stored = readStoredFieldValue(field, settings);

if (stored !== undefined && stored !== null && field.scale) {
// Stored in one unit, edited in another. File Sync's per-run limit is held
Expand All @@ -442,6 +432,26 @@
return stored === undefined || stored === null ? field.default : stored;
}

/**
* Read a field's saved value from where it is actually stored.
*
* Two descriptors name a nested home: `settings_path` as segments, `paths` as dotted
* strings. The draft is always keyed flat, because that is what the PATCH sends and what
* the server unpacks; only the stored read follows the path.
*
* Kept apart from `readFieldValue` because the connection tests, the dependency checks
* and the section status need the same lookup without the display default.
*/
export function readStoredFieldValue(field: AdminField, settings: Json): unknown {
if (field.settings_path) {
return readNestedValue(settings, field.settings_path);
}
if (field.paths?.length) {
return readNestedSetting(settings, field.paths[0]);
}
return field.key ? settings[field.key] : undefined;
}

/** Walk a dotted path into the settings document, or undefined if it is not there. */
export function readNestedSetting(settings: Json, path: string): unknown {
return readNestedValue(settings, String(path || '').split('.'));
Expand Down Expand Up @@ -591,13 +601,21 @@
}

/**
* Read the current value of a key another field depends on.
* Read a settings key's current value, preferring an unsaved edit over the stored one.
*
* A gate may itself be stored inside a nested object -- the document action limits are
* gated by an `enabled` flag that lives in the same container -- so the key alone is not
* enough to find the saved value.
* The key alone is not always enough to find the saved value. A declared field may be
* stored inside a nested object -- the document action limits are gated by an `enabled`
* flag that lives in their container, and the Web Search Foundry connection is kept inside

Check warning on line 608 in application/v2_ui/src/lib/adminFields.ts

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
* `web_search_agent` -- so the saved value is read through the field that declares the

Check warning on line 609 in application/v2_ui/src/lib/adminFields.ts

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 609 in application/v2_ui/src/lib/adminFields.ts

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
* key whenever the index knows one. Read as a top-level key instead, a saved Foundry
* endpoint comes back blank, and a connection test reports it missing while it is plainly
* on screen.
*
* Unlike `readFieldValue`, no schema default is substituted. A select shows its default
* before anything is stored, but a condition on that select must not hold until a value
* really is stored or chosen.
*/
function readDependencyValue(
export function readSettingValue(
key: string,
settings: Json,
draft: Json,
Expand All @@ -606,8 +624,8 @@
if (Object.prototype.hasOwnProperty.call(draft, key)) {
return draft[key];
}
const gate = fieldsByKey?.get(key);
return gate?.settings_path ? readNestedValue(settings, gate.settings_path) : settings[key];
const declared = fieldsByKey?.get(key);
return declared ? readStoredFieldValue(declared, settings) : settings[key];
}

/**
Expand All @@ -631,7 +649,7 @@
runtimeFlags?: Record<string, boolean>,
): boolean {
return !field.legacy && evaluateDependency(field.depends_on, (key) =>
readDependencyValue(key, settings, draft, fieldsByKey),
readSettingValue(key, settings, draft, fieldsByKey),
runtimeFlags);
}

Expand Down Expand Up @@ -703,6 +721,57 @@
return asString(current).trim() === String(expected);
}

/**
* Write a value at a dotted path, creating intermediate objects.
*
* Payload shapes are nested -- `direct.endpoint`, `apim.subscription_key` -- because that
* is what the existing test handlers expect. Declaring them flat and expanding here keeps
* the schema readable.
*/
function assignPath(target: Json, path: string, value: unknown): void {
const parts = path.split('.');
let cursor = target as Record<string, unknown>;

for (const part of parts.slice(0, -1)) {
if (typeof cursor[part] !== 'object' || cursor[part] === null) {
cursor[part] = {};
}
cursor = cursor[part] as Record<string, unknown>;
}

cursor[parts[parts.length - 1]] = value;
}

/**
* Build the request body a `connection-test` field declares, from the values on screen.
*
* Each source is read as an unsaved edit first and then as the saved value, found
* wherever its field declares it is stored. The Web Search Foundry connection is saved

Check warning on line 749 in application/v2_ui/src/lib/adminFields.ts

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
* inside `web_search_agent`, so reading its keys as top-level settings would test a

Check warning on line 750 in application/v2_ui/src/lib/adminFields.ts

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 750 in application/v2_ui/src/lib/adminFields.ts

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
* saved connection with blanks.
*
* A payload entry can be conditional, which is how one declaration covers both sides of
* an APIM-or-direct choice without sending the branch that is not in use.
*/
export function buildConnectionTestPayload(
field: AdminField,
settings: Json,
draft: Json,
fieldsByKey: Map<string, AdminField>,
): Json {
const read = (key: string): unknown => readSettingValue(key, settings, draft, fieldsByKey);

const payload: Json = { test_type: field.test_type };
for (const [path, source] of Object.entries(field.test_payload ?? {})) {
if (!evaluateDependency(source.when, read)) {
continue;
}
const value = source.key !== undefined ? read(source.key) : source.value;
assignPath(payload, path, value ?? '');
}
return payload;
}

/**
* Whether a field's cross-section prerequisite is satisfied.
*
Expand Down Expand Up @@ -745,7 +814,7 @@
if (Object.prototype.hasOwnProperty.call(runtimeFlags, condition)) {
return runtimeFlags[condition];
}
return asBoolean(readDependencyValue(condition, settings, draft, fieldsByKey));
return asBoolean(readSettingValue(condition, settings, draft, fieldsByKey));
}

/**
Expand Down
Loading
Loading