Skip to content
Merged
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
20 changes: 17 additions & 3 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import {
createCryptoProviderCache,
findFoldedRunByAddress,
lookupFoldedRunReconnectKey,
tagCredentialCipher,
} from "@corbits/folded-runs";
import { createTopLevelRunRoutes } from "@corbits/run-scope";
import {
Expand Down Expand Up @@ -513,6 +514,18 @@ export function credentialCipherFrom(
);
}

/**
* Hub-boot mint of the process-wide credential cipher: build from
* config, then runtime-tag the result. Missing or wrong-shape input
* fails closed — the hub does not boot.
*/
export function hubCredentialCipher(
config: HubConfig,
log: ReturnType<typeof getLogger>,
): CredentialCipher {
return tagCredentialCipher(credentialCipherFrom(config, log));
}

/**
* Instantiates one configured sidecar-provisioner backend. This is the
* extension point named in `.env.example` and `apps/hub/src/config.ts`:
Expand Down Expand Up @@ -571,9 +584,9 @@ export async function createHub(config: HubConfig) {
bus: mailboxBus,
});
const log = getLogger(["hub", "auth"]);
// Built once and shared by every secret-at-rest seam in this
// composition root — see `credentialCipherFrom`'s own doc comment.
const credentialCipher = credentialCipherFrom(config, log);
// Built once, tagged, and shared by every secret-at-rest seam in this
// composition root — see `hubCredentialCipher`.
const credentialCipher = hubCredentialCipher(config, log);

const auth = betterAuth({
baseURL: config.baseUrl,
Expand Down Expand Up @@ -2238,6 +2251,7 @@ export async function createHub(config: HubConfig) {
assetService,
sidecarRouter,
eventCollectors,
credentialCipher,
toolGrantsForPins,
mcpCredentialBindingsFor,
pinnedPackageCredentialBindingsFor,
Expand Down
34 changes: 33 additions & 1 deletion apps/hub/test/credential-cipher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import { describe, expect, test } from "bun:test";
import { getLogger } from "@intx/log";
import { tagCredentialCipher } from "@corbits/folded-runs";
import type { HubConfig } from "../src/config.ts";
import { credentialCipherFrom } from "../src/index.ts";
import { credentialCipherFrom, hubCredentialCipher } from "../src/index.ts";

const log = getLogger(["hub", "test"]);

Expand Down Expand Up @@ -75,3 +76,34 @@ describe("credentialCipherFrom", () => {
expect(encrypted).not.toBe("secret-value");
});
});

describe("hubCredentialCipher", () => {
test("boot tags the cipher built from a configured key", async () => {
const cipher = hubCredentialCipher(
{
...baseConfig,
credentialEncryptionKeyHex: "a".repeat(64),
},
log,
);
expect(cipher).toBe(tagCredentialCipher(cipher));
const encrypted = await cipher.encrypt("secret-value", "aad");
expect(encrypted).not.toBe("secret-value");
});

test("boot tags the noop cipher when ALLOW_PLAINTEXT_SECRETS is set", async () => {
const cipher = hubCredentialCipher(
{ ...baseConfig, allowPlaintextSecrets: true },
log,
);
expect(cipher).toBe(tagCredentialCipher(cipher));
const encrypted = await cipher.encrypt("secret-value", "aad");
expect(encrypted).toBe("secret-value");
});

test("boot still hard-fails when the key is missing and plaintext is not opted in", () => {
expect(() => hubCredentialCipher(baseConfig, log)).toThrow(
/CREDENTIAL_ENCRYPTION_KEY/,
);
});
});
18 changes: 8 additions & 10 deletions packages/chat/src/platform-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
resolveNewestProjectedDefinition,
sendFoldedMail,
wakeFoldedRun,
tagCredentialCipher,
type FoldedRunMode,
type FoldedRunsDeps,
type SendFoldedMailParams,
Expand Down Expand Up @@ -62,7 +63,7 @@ import { withTimeout } from "./with-timeout";
import { wrapWakeInferenceError } from "./model-unavailable";
import type { EventCollectorRegistry, SidecarRouter } from "@intx/hub-sessions";
import type { InferencePreference } from "@intx/agent";
import { formatRunAddress } from "@intx/types";
import { formatRunAddress, type CredentialCipher } from "@intx/types";
import type { FoldedBody } from "@intx/workflow-deploy";
import {
AgentUnreachableError,
Expand All @@ -87,13 +88,11 @@ export type CreateHubChatPlatformDeps = {
/**
* Decrypts credential secrets when an invited agent's launch resolves
* inference sources against the tenant catalog — see
* `@corbits/folded-runs`' `FoldedRunsDeps.credentialCipher`; every
* invited-agent launch and wake needs it.
* Omitted, `resolveDefinitionSources` falls back to a noop cipher and
* hands the raw stored secret to the provider unchanged — correct only
* when the credential was itself written unencrypted.
* `@corbits/folded-runs`' `FoldedRunsDeps.credentialCipher`. Tagged at
* construction: missing or wrong-shape input fails closed and the
* platform is not minted.
*/
credentialCipher?: FoldedRunsDeps["credentialCipher"];
credentialCipher: CredentialCipher;
/**
* Every caller of `createHubChatPlatform` builds this via
* `createEventCollectorRegistry` and passes it through — without it,
Expand Down Expand Up @@ -240,16 +239,15 @@ export type HubChatPlatform = ChatPlatform & {
export function createHubChatPlatform(
deps: CreateHubChatPlatformDeps,
): HubChatPlatform {
const credentialCipher = tagCredentialCipher(deps.credentialCipher);
const foldedRunsDeps: FoldedRunsDeps = {
db: deps.db,
sessionService: deps.sessionService,
assetService: deps.assetService,
sidecarRouter: deps.sidecarRouter,
eventCollectors: deps.eventCollectors,
toolGrantsForPins: deps.toolGrantsForPins,
...(deps.credentialCipher !== undefined
? { credentialCipher: deps.credentialCipher }
: {}),
credentialCipher,
...(deps.mcpCredentialBindingsFor !== undefined
? { mcpCredentialBindingsFor: deps.mcpCredentialBindingsFor }
: {}),
Expand Down
Loading
Loading