Secretize Hermes join defaults

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-26 13:54:14 +00:00
1 parent 73e5bd8f4e
commit 6f21eae0a4
2 files changed
+173 -3

No files matched your search

@@ -1,8 +1,36 @@
import { describe, expect, it } from "vitest";
import { randomUUID } from "node:crypto";
import { mkdirSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { eq } from "drizzle-orm";
import {
companies,
companySecretProviderConfigs,
companySecretVersions,
companySecrets,
createDb,
invites,
joinRequests,
} from "@paperclipai/db";
import {
buildJoinDefaultsPayloadForAccept,
normalizeAgentDefaultsForJoin,
prepareAgentDefaultsPayloadForJoinPersistence,
} from "../routes/access.js";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres invite accept gateway defaults tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
describe("buildJoinDefaultsPayloadForAccept (openclaw_gateway)", () => {
it("leaves non-gateway payloads unchanged", () => {
@@ -167,3 +195,112 @@ describe("normalizeAgentDefaultsForJoin (hermes_gateway)", () => {
);
});
});
describeEmbeddedPostgres("prepareAgentDefaultsPayloadForJoinPersistence (hermes_gateway)", () => {
let stopDb: (() => Promise<void>) | null = null;
let db!: ReturnType<typeof createDb>;
const previousKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
const secretsTmpDir = path.join(os.tmpdir(), `paperclip-hermes-join-defaults-${randomUUID()}`);
beforeAll(async () => {
mkdirSync(secretsTmpDir, { recursive: true });
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = path.join(secretsTmpDir, "master.key");
const started = await startEmbeddedPostgresTestDatabase("hermes-join-defaults");
stopDb = started.cleanup;
db = createDb(started.connectionString);
}, 20_000);
afterEach(async () => {
await db.delete(joinRequests);
await db.delete(invites);
await db.delete(companySecretVersions);
await db.delete(companySecrets);
await db.delete(companySecretProviderConfigs);
await db.delete(companies);
});
afterAll(async () => {
await stopDb?.();
if (previousKeyFile === undefined) {
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
} else {
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = previousKeyFile;
}
rmSync(secretsTmpDir, { recursive: true, force: true });
});
it("stores a secret ref instead of the literal apiKey in join request defaults", async () => {
const companyId = randomUUID();
const inviteId = randomUUID();
const joinRequestId = randomUUID();
const literalApiKey = `hermes-key-${randomUUID()}`;
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(invites).values({
id: inviteId,
companyId,
inviteType: "company_join",
tokenHash: `invite-token-${randomUUID()}`,
allowedJoinTypes: "agent",
defaultsPayload: null,
expiresAt: new Date("2027-03-10T00:00:00.000Z"),
});
const joinDefaults = normalizeAgentDefaultsForJoin({
adapterType: "hermes_gateway",
defaultsPayload: {
apiBaseUrl: "https://hermes.example",
apiKey: literalApiKey,
paperclipApiUrl: "https://paperclip.example",
},
deploymentMode: "authenticated",
deploymentExposure: "private",
bindHost: "127.0.0.1",
allowedHostnames: [],
});
expect(joinDefaults.fatalErrors).toEqual([]);
const persistedDefaults = await prepareAgentDefaultsPayloadForJoinPersistence({
db,
companyId,
adapterType: "hermes_gateway",
normalized: joinDefaults.normalized,
});
await db.insert(joinRequests).values({
id: joinRequestId,
inviteId,
companyId,
requestType: "agent",
status: "pending_approval",
requestIp: "127.0.0.1",
agentName: "Hermes Gateway",
adapterType: "hermes_gateway",
capabilities: "Hermes gateway agent",
agentDefaultsPayload: persistedDefaults,
claimSecretHash: "claim-secret-hash",
claimSecretExpiresAt: new Date("2027-03-11T00:00:00.000Z"),
});
const persistedJoinRequest = await db
.select()
.from(joinRequests)
.where(eq(joinRequests.id, joinRequestId))
.then((rows) => rows[0]);
const storedPayload = persistedJoinRequest?.agentDefaultsPayload as Record<string, unknown>;
expect(JSON.stringify(storedPayload)).not.toContain(literalApiKey);
expect(storedPayload.apiKey).toMatchObject({
type: "secret_ref",
version: "latest",
});
const storedSecrets = await db.select().from(companySecrets);
expect(storedSecrets).toHaveLength(1);
expect((storedPayload.apiKey as { secretId: string }).secretId).toBe(storedSecrets[0]?.id);
});
});
+35 -2
View File
@@ -83,6 +83,7 @@ import {
} from "../board-claim.js";
import { claimFirstInstanceAdmin } from "../first-admin-claim.js";
import { getStorageService } from "../storage/index.js";
import { secretService } from "../services/secrets.js";
function hashToken(token: string) {
return createHash("sha256").update(token).digest("hex");
@@ -1010,6 +1011,24 @@ export function normalizeAgentDefaultsForJoin(input: {
return { normalized, diagnostics, fatalErrors };
}
export async function prepareAgentDefaultsPayloadForJoinPersistence(input: {
db: Db;
companyId: string;
adapterType: string | null;
normalized: Record<string, unknown> | null;
actor?: { userId?: string | null; agentId?: string | null };
}): Promise<Record<string, unknown> | null> {
if (input.adapterType !== "hermes_gateway" || !input.normalized) {
return input.normalized;
}
return secretService(input.db).normalizeAdapterConfigForPersistence(
input.companyId,
input.normalized,
{ adapterType: input.adapterType, actor: input.actor },
);
}
function toInviteSummaryResponse(
req: Request,
token: string,
@@ -3687,6 +3706,20 @@ export function accessRoutes(
throw badRequest(joinDefaults.fatalErrors.join("; "));
}
const persistedJoinDefaultsPayload =
requestType === "agent"
? await prepareAgentDefaultsPayloadForJoinPersistence({
db,
companyId,
adapterType,
normalized: joinDefaults.normalized,
actor: {
userId: req.actor.userId ?? null,
agentId: req.actor.agentId ?? null
}
})
: null;
if (requestType === "agent" && adapterType === "openclaw_gateway") {
logger.info(
{
@@ -3780,7 +3813,7 @@ export function accessRoutes(
? req.body.capabilities ?? null
: null,
agentDefaultsPayload:
requestType === "agent" ? joinDefaults.normalized : null,
requestType === "agent" ? persistedJoinDefaultsPayload : null,
claimSecretHash,
claimSecretExpiresAt
})
@@ -3806,7 +3839,7 @@ export function accessRoutes(
: null,
adapterType: requestType === "agent" ? adapterType : null,
agentDefaultsPayload:
requestType === "agent" ? joinDefaults.normalized : null,
requestType === "agent" ? persistedJoinDefaultsPayload : null,
updatedAt: new Date()
})
.where(eq(joinRequests.id, replayJoinRequestId as string))