mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Secretize Hermes join defaults
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user