Add getConfigSchema to surface adapter config fields in UI
- src/server/config-schema.ts: define schema with Core, Kubernetes, and Operational field groups matching agentConfigurationDoc - src/server/config-schema.test.ts: 10 tests covering field types, defaults, options, and group structure - src/server/index.ts: attach getConfigSchema to ServerAdapterModule - Revert k8s-client.ts loadFromConfig change (not available in installed @kubernetes/client-node version) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { getConfigSchema } from "./config-schema.js";
|
||||||
|
|
||||||
|
interface ConfigFieldSchema {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
default?: unknown;
|
||||||
|
options?: { label: string; value: string }[];
|
||||||
|
required?: boolean;
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getConfigSchema", () => {
|
||||||
|
it("returns a schema with expected field groups", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
expect(schema.fields.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const groups = schema.fields.map((f: ConfigFieldSchema) => f.group);
|
||||||
|
const uniqueGroups = [...new Set(groups)];
|
||||||
|
|
||||||
|
expect(uniqueGroups).toContain("Core");
|
||||||
|
expect(uniqueGroups).toContain("Kubernetes");
|
||||||
|
expect(uniqueGroups).toContain("Operational");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has model as required text field", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const modelField = schema.fields.find((f: ConfigFieldSchema) => f.key === "model");
|
||||||
|
expect(modelField).toBeDefined();
|
||||||
|
expect(modelField!.type).toBe("text");
|
||||||
|
expect(modelField!.required).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has imagePullPolicy as select with correct options", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "imagePullPolicy");
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("select");
|
||||||
|
expect(field!.options).toEqual([
|
||||||
|
{ label: "IfNotPresent", value: "IfNotPresent" },
|
||||||
|
{ label: "Always", value: "Always" },
|
||||||
|
{ label: "Never", value: "Never" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dangerouslySkipPermissions defaults to true", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "dangerouslySkipPermissions");
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("toggle");
|
||||||
|
expect(field!.default).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ttlSecondsAfterFinished defaults to 300", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "ttlSecondsAfterFinished");
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("number");
|
||||||
|
expect(field!.default).toBe(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("timeoutSec defaults to 0", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "timeoutSec");
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("number");
|
||||||
|
expect(field!.default).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("graceSec defaults to 60", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "graceSec");
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("number");
|
||||||
|
expect(field!.default).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retainJobs is a toggle", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === "retainJobs");
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("toggle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has all kubernetes resource fields", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const resourceKeys = [
|
||||||
|
"resources.requests.cpu",
|
||||||
|
"resources.requests.memory",
|
||||||
|
"resources.limits.cpu",
|
||||||
|
"resources.limits.memory",
|
||||||
|
];
|
||||||
|
for (const key of resourceKeys) {
|
||||||
|
const field = schema.fields.find((f: ConfigFieldSchema) => f.key === key);
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe("text");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has env and extraArgs as textarea", () => {
|
||||||
|
const schema = getConfigSchema();
|
||||||
|
const envField = schema.fields.find((f: ConfigFieldSchema) => f.key === "env");
|
||||||
|
expect(envField).toBeDefined();
|
||||||
|
expect(envField!.type).toBe("textarea");
|
||||||
|
|
||||||
|
const extraArgsField = schema.fields.find((f: ConfigFieldSchema) => f.key === "extraArgs");
|
||||||
|
expect(extraArgsField).toBeDefined();
|
||||||
|
expect(extraArgsField!.type).toBe("textarea");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// Local type augmentation for adapter-utils version that has getConfigSchema.
|
||||||
|
// The deployed environment has a newer version of adapter-utils with these types.
|
||||||
|
interface ConfigFieldSchema {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: "text" | "select" | "toggle" | "number" | "textarea" | "combobox";
|
||||||
|
options?: { label: string; value: string }[];
|
||||||
|
default?: unknown;
|
||||||
|
hint?: string;
|
||||||
|
required?: boolean;
|
||||||
|
group?: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdapterConfigSchema {
|
||||||
|
fields: ConfigFieldSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigSchema(): AdapterConfigSchema {
|
||||||
|
return {
|
||||||
|
fields: [
|
||||||
|
// Core fields
|
||||||
|
{
|
||||||
|
key: "model",
|
||||||
|
label: "Model",
|
||||||
|
type: "text",
|
||||||
|
hint: "OpenCode model id in provider/model format (e.g. anthropic/claude-sonnet-4-6)",
|
||||||
|
required: true,
|
||||||
|
group: "Core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "variant",
|
||||||
|
label: "Variant",
|
||||||
|
type: "text",
|
||||||
|
hint: "Provider-specific reasoning/profile variant passed as --variant",
|
||||||
|
group: "Core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dangerouslySkipPermissions",
|
||||||
|
label: "Skip Permission Checks",
|
||||||
|
type: "toggle",
|
||||||
|
default: true,
|
||||||
|
hint: "Inject runtime config with permission.external_directory=allow",
|
||||||
|
group: "Core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "promptTemplate",
|
||||||
|
label: "Prompt Template",
|
||||||
|
type: "text",
|
||||||
|
hint: "Run prompt template",
|
||||||
|
group: "Core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "extraArgs",
|
||||||
|
label: "Extra Arguments",
|
||||||
|
type: "textarea",
|
||||||
|
hint: "JSON array of additional CLI args appended to the opencode command",
|
||||||
|
group: "Core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "env",
|
||||||
|
label: "Environment Variables",
|
||||||
|
type: "textarea",
|
||||||
|
hint: "KEY=VALUE pairs, one per line. Overrides inherited vars from the Deployment.",
|
||||||
|
group: "Core",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Kubernetes fields
|
||||||
|
{
|
||||||
|
key: "namespace",
|
||||||
|
label: "Namespace",
|
||||||
|
type: "text",
|
||||||
|
hint: "Kubernetes namespace for Jobs; defaults to the Deployment namespace",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "image",
|
||||||
|
label: "Container Image",
|
||||||
|
type: "text",
|
||||||
|
hint: "Override container image; defaults to the running Deployment image",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "imagePullPolicy",
|
||||||
|
label: "Image Pull Policy",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "IfNotPresent", value: "IfNotPresent" },
|
||||||
|
{ label: "Always", value: "Always" },
|
||||||
|
{ label: "Never", value: "Never" },
|
||||||
|
],
|
||||||
|
default: "IfNotPresent",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "kubeconfig",
|
||||||
|
label: "Kubeconfig Path",
|
||||||
|
type: "text",
|
||||||
|
hint: "Absolute path to a kubeconfig file; defaults to in-cluster service account auth",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "resources.requests.cpu",
|
||||||
|
label: "CPU Request",
|
||||||
|
type: "text",
|
||||||
|
hint: "e.g. '1000m' or '1'",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "resources.requests.memory",
|
||||||
|
label: "Memory Request",
|
||||||
|
type: "text",
|
||||||
|
hint: "e.g. '2Gi' or '2G'",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "resources.limits.cpu",
|
||||||
|
label: "CPU Limit",
|
||||||
|
type: "text",
|
||||||
|
hint: "e.g. '4000m' or '4'",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "resources.limits.memory",
|
||||||
|
label: "Memory Limit",
|
||||||
|
type: "text",
|
||||||
|
hint: "e.g. '8Gi' or '8G'",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nodeSelector",
|
||||||
|
label: "Node Selector",
|
||||||
|
type: "textarea",
|
||||||
|
hint: "key=value pairs, one per line",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tolerations",
|
||||||
|
label: "Tolerations",
|
||||||
|
type: "textarea",
|
||||||
|
hint: "JSON array of toleration objects",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: "Labels",
|
||||||
|
type: "textarea",
|
||||||
|
hint: "key=value pairs, one per line. Extra labels added to Job metadata.",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ttlSecondsAfterFinished",
|
||||||
|
label: "TTL After Finished",
|
||||||
|
type: "number",
|
||||||
|
default: 300,
|
||||||
|
hint: "Auto-cleanup delay in seconds after Job completes",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "retainJobs",
|
||||||
|
label: "Retain Jobs",
|
||||||
|
type: "toggle",
|
||||||
|
hint: "Skip cleanup on completion for debugging",
|
||||||
|
group: "Kubernetes",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Operational fields
|
||||||
|
{
|
||||||
|
key: "timeoutSec",
|
||||||
|
label: "Timeout (seconds)",
|
||||||
|
type: "number",
|
||||||
|
default: 0,
|
||||||
|
hint: "Run timeout in seconds; 0 means no timeout",
|
||||||
|
group: "Operational",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "graceSec",
|
||||||
|
label: "Grace Period (seconds)",
|
||||||
|
type: "number",
|
||||||
|
default: 60,
|
||||||
|
hint: "Additional grace before adapter gives up after Job deadline",
|
||||||
|
group: "Operational",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
+3
-1
@@ -3,6 +3,7 @@ import { type, models, agentConfigurationDoc } from "../index.js";
|
|||||||
import { execute } from "./execute.js";
|
import { execute } from "./execute.js";
|
||||||
import { testEnvironment } from "./test.js";
|
import { testEnvironment } from "./test.js";
|
||||||
import { sessionCodec } from "./session.js";
|
import { sessionCodec } from "./session.js";
|
||||||
|
import { getConfigSchema } from "./config-schema.js";
|
||||||
|
|
||||||
export function createServerAdapter(): ServerAdapterModule {
|
export function createServerAdapter(): ServerAdapterModule {
|
||||||
return {
|
return {
|
||||||
@@ -13,7 +14,8 @@ export function createServerAdapter(): ServerAdapterModule {
|
|||||||
models,
|
models,
|
||||||
supportsLocalAgentJwt: true,
|
supportsLocalAgentJwt: true,
|
||||||
agentConfigurationDoc,
|
agentConfigurationDoc,
|
||||||
};
|
getConfigSchema,
|
||||||
|
} as ServerAdapterModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { execute, testEnvironment, sessionCodec };
|
export { execute, testEnvironment, sessionCodec };
|
||||||
|
|||||||
Reference in New Issue
Block a user