Skip to content
Open
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
10 changes: 8 additions & 2 deletions ui/src/api/domain/agentTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ export interface AgentTemplateLocalRef {
name: string;
}

/** A model or MCP reference, local when namespace is omitted. */
/**
* A model or MCP reference, local when namespace is omitted. Omitted `group` and
* `kind` mean the default kind; the form keeps them so an edit cannot repoint a reference.
*/
export interface ObjectReference {
group?: string;
kind?: string;
name: string;
namespace?: string;
}
Expand All @@ -31,7 +36,8 @@ export interface ConfigMapKeyRef {
* non-empty list limits the binding to those names.
*/
export interface McpToolBinding {
server: ObjectReference & { kind: "RemoteMCPServer" };
/** The API server stores `kind: RemoteMCPServer` when it is omitted. */
server: ObjectReference;
tools?: string[];
requireApproval?: boolean;
}
Expand Down
15 changes: 14 additions & 1 deletion ui/src/components/agent-template-form/agentTemplateDraft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,20 @@ describe("cross-namespace references", () => {
};
const draft = draftFromSpec(spec, "team-a");
expect(draft.modelConfig).toEqual({ name: "default", namespace: "shared" });
expect(draft.mcpTools[0].serverRef).toEqual({ name: "tools", namespace: "shared" });
expect(draft.mcpTools[0].serverRef).toEqual({ kind: "RemoteMCPServer", name: "tools", namespace: "shared" });
const saved = specFromDraft(draft, spec);
expect(saved.modelConfig).toEqual(spec.modelConfig);
expect(saved.tools).toEqual(spec.tools);
});

// Dropping group or kind would point the reference at a same-named default object.
it("keeps the group and kind of model and MCP references through an edit", () => {
const spec = {
modelConfig: { group: "example.com", kind: "Foo", name: "default" },
tools: [{ mcp: { server: { group: "example.com", kind: "Foo", name: "tools" } } }],
};
const draft = draftFromSpec(spec, "team-a");
draft.description = "edited";
const saved = specFromDraft(draft, spec);
expect(saved.modelConfig).toEqual(spec.modelConfig);
expect(saved.tools).toEqual(spec.tools);
Expand Down
15 changes: 10 additions & 5 deletions ui/src/components/agent-template-form/agentTemplateDraft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function draftFromSpec(spec: AgentTemplateSpec, namespace: string): Agent
mcpTools: tools
.filter((binding) => binding.mcp)
.map((binding) => ({
serverRef: { name: binding.mcp?.server.name ?? "", namespace: binding.mcp?.server.namespace },
serverRef: { ...binding.mcp?.server, name: binding.mcp?.server.name ?? "" },
tools: [...(binding.mcp?.tools ?? [])],
requireApproval: binding.mcp?.requireApproval,
})),
Expand Down Expand Up @@ -167,8 +167,8 @@ export function specFromDraft(
.filter((tool) => tool.serverRef.name.trim() !== "")
.map((tool) => ({
mcp: {
// The form only selects RemoteMCPServers.
server: { kind: "RemoteMCPServer" as const, ...normalizedReference(tool.serverRef, draft.namespace) },
// A new pick is a RemoteMCPServer; a loaded reference keeps its own group and kind.
server: { kind: "RemoteMCPServer", ...normalizedReference(tool.serverRef, draft.namespace) },
...(tool.tools.length > 0 ? { tools: [...tool.tools] } : {}),
// omitempty on the CRD: false is the default, so only true is sent.
...(tool.requireApproval ? { requireApproval: true } : {}),
Expand Down Expand Up @@ -313,11 +313,16 @@ export function draftProblems(
return problems;
}

/** Keep local manifests concise while preserving shared resource identity. */
/** Keep local manifests concise while preserving shared resource identity, group and kind. */
function normalizedReference(ref: ObjectReference, ownerNamespace: string): ObjectReference {
const name = ref.name.trim();
const namespace = ref.namespace?.trim();
return !namespace || namespace === ownerNamespace ? { name } : { name, namespace };
return {
...(ref.group ? { group: ref.group } : {}),
...(ref.kind ? { kind: ref.kind } : {}),
name,
...(namespace && namespace !== ownerNamespace ? { namespace } : {}),
};
}

/** Parses only the object-valued JSON shape the public API accepts. */
Expand Down
Loading