diff --git a/server/src/__tests__/adapter-registry.test.ts b/server/src/__tests__/adapter-registry.test.ts index cb0e007e..983011fe 100644 --- a/server/src/__tests__/adapter-registry.test.ts +++ b/server/src/__tests__/adapter-registry.test.ts @@ -61,12 +61,14 @@ describe("server adapter registry", () => { unregisterServerAdapter("external_test"); unregisterServerAdapter("claude_local"); setOverridePaused("claude_local", false); + setOverridePaused("hermes_local", true); }); afterEach(() => { unregisterServerAdapter("external_test"); unregisterServerAdapter("claude_local"); setOverridePaused("claude_local", false); + setOverridePaused("hermes_local", false); hermesExecuteMock.mockClear(); }); @@ -385,6 +387,105 @@ describe("server adapter registry", () => { expect(patchedCtx.agent.adapterConfig.env.PAPERCLIP_API_KEY).toBe("agent-run-jwt"); }); + it("passes Hermes custom providers through extraArgs while injecting auth", async () => { + const adapter = requireServerAdapter("hermes_local"); + + await adapter.execute({ + runId: "run-123", + agent: { + id: "agent-123", + companyId: "company-123", + name: "Hermes Agent", + role: "engineer", + adapterType: "hermes_local", + adapterConfig: { + provider: "custom:paperclip-openai", + extraArgs: ["--source", "tool"], + }, + }, + runtime: {}, + config: {}, + context: {}, + onLog: async () => {}, + onMeta: async () => {}, + onSpawn: async () => {}, + authToken: "agent-run-jwt", + }); + + const [patchedCtx] = hermesExecuteMock.mock.calls[0]; + expect(patchedCtx.agent.adapterConfig.provider).toBe("custom:paperclip-openai"); + expect(patchedCtx.agent.adapterConfig.extraArgs).toEqual([ + "--source", + "tool", + "--provider", + "custom:paperclip-openai", + ]); + expect(patchedCtx.agent.adapterConfig.env.PAPERCLIP_API_KEY).toBe("agent-run-jwt"); + }); + + it("does not duplicate an explicit Hermes provider extraArg", async () => { + const adapter = requireServerAdapter("hermes_local"); + + await adapter.execute({ + runId: "run-123", + agent: { + id: "agent-123", + companyId: "company-123", + name: "Hermes Agent", + role: "engineer", + adapterType: "hermes_local", + adapterConfig: { + provider: "custom:paperclip-openai", + extraArgs: ["--provider=custom:paperclip-openai"], + }, + }, + runtime: {}, + config: {}, + context: {}, + onLog: async () => {}, + onMeta: async () => {}, + onSpawn: async () => {}, + authToken: "agent-run-jwt", + }); + + const [patchedCtx] = hermesExecuteMock.mock.calls[0]; + expect(patchedCtx.agent.adapterConfig.extraArgs).toEqual([ + "--provider=custom:paperclip-openai", + ]); + }); + + it("does not duplicate spaced Hermes provider extraArgs", async () => { + const adapter = requireServerAdapter("hermes_local"); + + await adapter.execute({ + runId: "run-123", + agent: { + id: "agent-123", + companyId: "company-123", + name: "Hermes Agent", + role: "engineer", + adapterType: "hermes_local", + adapterConfig: { + provider: "custom:paperclip-openai", + extraArgs: ["--provider", "custom:paperclip-openai"], + }, + }, + runtime: {}, + config: {}, + context: {}, + onLog: async () => {}, + onMeta: async () => {}, + onSpawn: async () => {}, + authToken: "agent-run-jwt", + }); + + const [patchedCtx] = hermesExecuteMock.mock.calls[0]; + expect(patchedCtx.agent.adapterConfig.extraArgs).toEqual([ + "--provider", + "custom:paperclip-openai", + ]); + }); + it("passes the original Hermes context through when authToken is absent", async () => { const adapter = requireServerAdapter("hermes_local"); const ctx = { diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 55bce77b..09f6b2a2 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -214,6 +214,24 @@ function normalizeHermesConfig( return ctx; } +function passHermesCustomProviderThroughExtraArgs(config: Record): Record { + const provider = typeof config.provider === "string" ? config.provider.trim() : ""; + if (!provider.startsWith("custom:")) return config; + + const existingExtraArgs = Array.isArray(config.extraArgs) + ? config.extraArgs.filter((arg): arg is string => typeof arg === "string") + : []; + const alreadyHasProviderArg = existingExtraArgs.some((arg) => + arg === "--provider" || arg.startsWith("--provider=") + ); + if (alreadyHasProviderArg) return config; + + return { + ...config, + extraArgs: [...existingExtraArgs, "--provider", provider], + }; +} + function dedupeAdapterModels(models: AdapterModel[]): AdapterModel[] { const seen = new Set(); const result: AdapterModel[] = []; @@ -470,19 +488,20 @@ const hermesLocalAdapter: ServerAdapterModule = { PAPERCLIP_RUN_ID: normalizedCtx.runId, }, }; + const effectivePatchedConfig = passHermesCustomProviderThroughExtraArgs(patchedConfig); // Only inject the auth guard into promptTemplate when a custom template already exists. // When no custom template is set, Hermes uses its built-in default heartbeat/task prompt — // overwriting it with only the auth guard text would strip the assigned issue/workflow instructions. if (promptTemplate) { - patchedConfig.promptTemplate = `${authGuardPrompt}\n\n${promptTemplate}`; + effectivePatchedConfig.promptTemplate = `${authGuardPrompt}\n\n${promptTemplate}`; } const patchedCtx = { ...normalizedCtx, agent: { ...normalizedCtx.agent, - adapterConfig: patchedConfig, + adapterConfig: effectivePatchedConfig, }, };