diff --git a/apps/controller/src/composition/repository-credentials/platform.ts b/apps/controller/src/composition/repository-credentials/platform.ts index bdafc7366b..ac39d311aa 100644 --- a/apps/controller/src/composition/repository-credentials/platform.ts +++ b/apps/controller/src/composition/repository-credentials/platform.ts @@ -45,14 +45,13 @@ export async function composeRepositoryAccess(input: { if (selection.implementation !== "github" || selection.id !== definition.drivers.repo) { throw new Error("The repository credential Driver must match its owning Backend."); } - // Native sending has its own transport validation; it is not a Repo Driver. - if (!input.protectedGit) { - GitHubNativeRepositorySender.validateConfiguration(selection.configuration); - } - const configuration = ( - input.protectedGit - ? requireConfiguration(GitHubRepositoryPolicy, selection.configuration) - : selection.configuration + // Native sending has its own transport contract; it is not a Repo Driver. Each contract + // refuses at load, before any Backend is built (finding 2214). The merged native + // configuration is checked only here: the Repo Driver's installation parser stays inside + // the repository credential import boundary. + const configuration = requireConfiguration( + input.protectedGit ? GitHubRepositoryPolicy : GitHubNativeRepositorySender, + selection.configuration, ) as Readonly<{ controlSocket?: string; sessionDurationSeconds: number; diff --git a/apps/controller/src/drivers/repo/github/installation.ts b/apps/controller/src/drivers/repo/github/installation.ts index eff8ac19b1..bfbeecab7b 100644 --- a/apps/controller/src/drivers/repo/github/installation.ts +++ b/apps/controller/src/drivers/repo/github/installation.ts @@ -9,7 +9,6 @@ import { } from "../../../composition/installation-fields.ts"; import type { SelectedDriverConfiguration } from "../../../composition/installation-config.ts"; import type { PreparedRepositoryGitConfiguration } from "../../../backends/repository-credentials/prepared-git-owner.ts"; -import { GitHubNativeRepositorySender } from "./driver.ts"; import { preparedGitHubConfiguration } from "./credentials/prepared-configuration.ts"; import type { GitHubRepositoryRegistry } from "./credentials/registry.ts"; @@ -35,10 +34,6 @@ export function githubRepositoryConfiguration(input: unknown, mode: "development controlSocket: nonempty(native.controlSocket, "repository.nativeSender.controlSocket"), publicCaPath: nonempty(native.publicCaPath, "repository.nativeSender.publicCaPath"), }; - GitHubNativeRepositorySender.validateConfiguration({ - ...object(selection.configuration, "repository.configuration"), - ...nativeSender, - }); } return { selection, diff --git a/tests/integration/configuration-startup.test.mjs b/tests/integration/configuration-startup.test.mjs index 3925998c38..70d732db7b 100644 --- a/tests/integration/configuration-startup.test.mjs +++ b/tests/integration/configuration-startup.test.mjs @@ -889,6 +889,17 @@ test("repository startup constructs the same local resolver without a private so assert.equal(startupDiagnostic(server.stderr, "startup-error").code, "PERSISTENCE_UNAVAILABLE"); }); +function nativeSenderRefusal(path, code) { + return (error) => { + assert.equal(error.name, "ConfigurationValidationError"); + assert.deepEqual( + error.error.details.map((detail) => [detail.path, detail.code]), + [[path, code]], + ); + return true; + }; +} + test("repository startup rejects unmatched ownership, registry identity, duration and CA inputs", async (t) => { const { configuration: baseline, tls, writableCaPath } = await repositoryInstallation(t); for (const [mutate, expected] of [ @@ -925,6 +936,14 @@ test("repository startup rejects unmatched ownership, registry identity, duratio ], [(value) => (value.repository.nativeSender.publicCaPath = tls.keyFile), /invalid-public-ca/], [(value) => (value.repository.nativeSender.publicCaPath = writableCaPath), /invalid-public-ca/], + // The native sender's path contract (finding 2214): an existing CA file under a + // non-canonical path is refused at load. The control socket client checks its own path. + [ + (value) => + (value.repository.nativeSender.publicCaPath = + value.repository.nativeSender.publicCaPath.replace(/\/ca\.crt$/, "/./ca.crt")), + nativeSenderRefusal("", "INVALID_VALUE"), + ], [(value) => (value.repository.package = "@example/driver"), /unsupported option package/], [(value) => (value.repository.implementation = "other"), /unsupported option implementation/], [(value) => (value.drivers.compute.id = "compute-ssh"), /bundled Kubernetes/],