fix(adapters): pass Hermes custom providers as args (#8231)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent adapters translate Paperclip configuration into the CLI/runtime flags needed by each provider. > - Hermes custom providers are represented as `custom:*` values, but the adapter registry was not passing that provider value through as Hermes CLI arguments. > - The fix is isolated to adapter argument construction and its regression tests. > - This pull request extracts only the Hermes custom-provider pass-through fix onto `origin/master`. > - The benefit is a small adapter PR that can merge independently from UI, skills catalog, and migration work. ## Linked Issues or Issue Description No GitHub issue exists for this branch split. Internal source task: [PAP-11234](/PAP/issues/PAP-11234). Problem/motivation: - Hermes `custom:*` providers need to reach the Hermes process as `--provider <value>`. - The adapter should preserve existing auth injection and avoid adding duplicate provider flags when the user already supplied one. Proposed solution: - Detect Hermes `custom:*` provider values in adapter registry argument construction. - Add `--provider <custom value>` unless an explicit provider arg already exists. - Cover both spaced and equals-style existing provider args in regression tests. Related PR search: - Found related Hermes adapter PRs such as #7544 and #3027, but none duplicates this specific custom-provider arg pass-through behavior. Roadmap alignment: - Checked `ROADMAP.md`; no duplicate planned item was found for this adapter fix. ## What Changed - Updated Hermes adapter registration argument construction to pass `custom:*` providers through `extraArgs` as `--provider <value>`. - Preserved existing auth injection behavior. - Avoided duplicate provider arguments when `--provider value` or `--provider=value` is already present. - Added adapter registry regression coverage. ## Verification - `CI=true NODE_ENV=development pnpm install --frozen-lockfile --prefer-offline` - `pnpm exec vitest server/src/__tests__/adapter-registry.test.ts --run` — 1 file, 20 tests passed. ## Risks - Low risk; change is scoped to Hermes adapter argument construction. - Review should confirm the provider flag syntax matches current Hermes CLI expectations. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI GPT-5 Codex via Paperclip `codex_local` / CodexCoder, GPT-5-class coding model with tool use and shell execution. Exact runtime snapshot and context-window setting were not exposed by the Paperclip run context. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details available from the run context) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots (N/A; no UI change) - [x] I have updated relevant documentation to reflect my changes (N/A; no docs behavior changed) - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green (pending on draft PR) - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups (pending follow-up loop) - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -214,6 +214,24 @@ function normalizeHermesConfig<T extends { config?: unknown; agent?: unknown }>(
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function passHermesCustomProviderThroughExtraArgs(config: Record<string, unknown>): Record<string, unknown> {
|
||||
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<string>();
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user