P3 Slice Zero, Task 4 — replace Web free-text selection with the structured harness catalog (#1170)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: Jason Woltje <[email protected]>
This commit was merged in pull request #1170.
This commit is contained in:
@@ -5,6 +5,14 @@
|
||||
* a non-array, `.toFixed` on a non-number) or render an object as a React
|
||||
* child.
|
||||
*/
|
||||
import type {
|
||||
HarnessAuthState,
|
||||
HarnessCatalog,
|
||||
HarnessCatalogEntry,
|
||||
HarnessModelAvailability,
|
||||
HarnessSelection,
|
||||
HarnessSummary,
|
||||
} from '@/lib/types';
|
||||
|
||||
export function asString(value: unknown, fallback = ''): string {
|
||||
return typeof value === 'string' ? value : fallback;
|
||||
@@ -38,6 +46,99 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP harness/catalog/selection JSON bodies are as untrusted as the socket
|
||||
* payloads above — a misbehaving or compromised gateway can send anything. The
|
||||
* guards below normalize those bodies into the typed client shapes without ever
|
||||
* rendering a raw body, so a 404/422/malformed response can never inject an
|
||||
* object into React or a non-tuple into the selection state.
|
||||
*/
|
||||
|
||||
/** Normalizes an untrusted `authState` to the closed set, defaulting to the
|
||||
* safest value (`unavailable`) for anything unrecognized. */
|
||||
export function asHarnessAuthState(value: unknown): HarnessAuthState {
|
||||
return value === 'ready' || value === 'auth_required' || value === 'unavailable'
|
||||
? value
|
||||
: 'unavailable';
|
||||
}
|
||||
|
||||
/** Normalizes an untrusted `availability` to the closed set, defaulting to
|
||||
* `unavailable` so a malformed row can never present as sendable. */
|
||||
export function asHarnessAvailability(value: unknown): HarnessModelAvailability {
|
||||
return value === 'available' ? 'available' : 'unavailable';
|
||||
}
|
||||
|
||||
/** A tuple is valid only when all three ids are non-empty strings — a partial
|
||||
* or malformed selection is rejected (null) rather than half-adopted. */
|
||||
export function asHarnessSelection(value: unknown): HarnessSelection | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const harnessId = value.harnessId;
|
||||
const providerId = value.providerId;
|
||||
const modelId = value.modelId;
|
||||
if (
|
||||
typeof harnessId !== 'string' ||
|
||||
typeof providerId !== 'string' ||
|
||||
typeof modelId !== 'string' ||
|
||||
harnessId.length === 0 ||
|
||||
providerId.length === 0 ||
|
||||
modelId.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { harnessId, providerId, modelId };
|
||||
}
|
||||
|
||||
/** Normalizes an untrusted array into typed harness summaries, dropping any row
|
||||
* without a usable id. */
|
||||
export function asHarnessSummaries(value: unknown): HarnessSummary[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const out: HarnessSummary[] = [];
|
||||
for (const item of value) {
|
||||
if (!isRecord(item)) continue;
|
||||
const id = asString(item.id);
|
||||
if (id.length === 0) continue;
|
||||
out.push({
|
||||
id,
|
||||
displayName: asNonEmptyString(item.displayName, id),
|
||||
capabilities: asStringArray(item.capabilities),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function asHarnessCatalogEntry(value: unknown): HarnessCatalogEntry | null {
|
||||
const selection = asHarnessSelection(value);
|
||||
if (selection === null || !isRecord(value)) return null;
|
||||
return {
|
||||
...selection,
|
||||
displayName: asNonEmptyString(value.displayName, selection.modelId),
|
||||
reasoningCapability: value.reasoningCapability === true,
|
||||
inputTypes: asStringArray(value.inputTypes),
|
||||
authState: asHarnessAuthState(value.authState),
|
||||
availability: asHarnessAvailability(value.availability),
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalizes an untrusted catalog body into the typed client catalog. The
|
||||
* caller supplies `harnessId` (from the request path) so the returned catalog
|
||||
* is scoped to the harness that was actually requested, never a body-echoed id.
|
||||
* Malformed model rows are dropped rather than invalidating the whole catalog. */
|
||||
export function asHarnessCatalog(value: unknown, harnessId: string): HarnessCatalog {
|
||||
const record = isRecord(value) ? value : {};
|
||||
const rawModels = Array.isArray(record.models) ? record.models : [];
|
||||
const models: HarnessCatalogEntry[] = [];
|
||||
for (const row of rawModels) {
|
||||
const entry = asHarnessCatalogEntry(row);
|
||||
if (entry !== null) models.push(entry);
|
||||
}
|
||||
return {
|
||||
harnessId,
|
||||
version: asString(record.version),
|
||||
fingerprint: asString(record.fingerprint),
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
/** The single point of truth for what counts as a valid conversation ID
|
||||
* anywhere a scoped server event may adopt one into state — a non-empty
|
||||
* string, nothing else. Every site that establishes or compares
|
||||
|
||||
Reference in New Issue
Block a user