From 933a3b1e5bb1ad309477f8ca4ed021795bf55d04 Mon Sep 17 00:00:00 2001 From: freeqaz Date: Thu, 22 Oct 2026 23:44:12 +0000 Subject: [PATCH 1/3] fix(repository): validate the native sender configuration at load #2487 replaced requireConfiguration(GitHubRepoDriver, ...) for native Git with a call to GitHubNativeRepositorySender.validateConfiguration whose result was discarded, and the native branch skipped the schema. A non-canonical publicCaPath, a fractional sessionDurationSeconds or an unknown option was accepted. Run requireConfiguration with the native sender's contract in both places, as the protected path already does with the repository policy. --- .../repository-credentials/platform.ts | 13 ++++----- .../src/drivers/repo/github/installation.ts | 7 +++-- .../configuration-startup.test.mjs | 27 +++++++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/apps/controller/src/composition/repository-credentials/platform.ts b/apps/controller/src/composition/repository-credentials/platform.ts index bdafc7366b..3c724a53b0 100644 --- a/apps/controller/src/composition/repository-credentials/platform.ts +++ b/apps/controller/src/composition/repository-credentials/platform.ts @@ -45,14 +45,11 @@ 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. Both contracts + // refuse at load, before any Backend is built (finding 2214). + 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..3247eabbb4 100644 --- a/apps/controller/src/drivers/repo/github/installation.ts +++ b/apps/controller/src/drivers/repo/github/installation.ts @@ -1,6 +1,9 @@ import { createHash } from "node:crypto"; import { SandboxRepositoryRecipientUnverifiedError } from "@openclaw-enterprise/occ"; -import type { GitHubRepositoryCredentialBackendDefinition } from "@openclaw-enterprise/contracts"; +import { + requireConfiguration, + type GitHubRepositoryCredentialBackendDefinition, +} from "@openclaw-enterprise/contracts"; import { closed, nonempty, @@ -35,7 +38,7 @@ export function githubRepositoryConfiguration(input: unknown, mode: "development controlSocket: nonempty(native.controlSocket, "repository.nativeSender.controlSocket"), publicCaPath: nonempty(native.publicCaPath, "repository.nativeSender.publicCaPath"), }; - GitHubNativeRepositorySender.validateConfiguration({ + requireConfiguration(GitHubNativeRepositorySender, { ...object(selection.configuration, "repository.configuration"), ...nativeSender, }); diff --git a/tests/integration/configuration-startup.test.mjs b/tests/integration/configuration-startup.test.mjs index 3925998c38..a3e9e7fcef 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,22 @@ 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 own configuration contract (finding 2214): an existing CA file under a + // non-canonical path, a fractional duration and an unknown option are refused at load. + [ + (value) => + (value.repository.nativeSender.publicCaPath = + value.repository.nativeSender.publicCaPath.replace(/\/ca\.crt$/, "/./ca.crt")), + nativeSenderRefusal("", "INVALID_VALUE"), + ], + [ + (value) => (value.repository.configuration.sessionDurationSeconds = 600.5), + nativeSenderRefusal("/sessionDurationSeconds", "INVALID_TYPE"), + ], + [ + (value) => (value.repository.configuration.unexpected = true), + nativeSenderRefusal("", "UNKNOWN_FIELD"), + ], [(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/], From 87c38f94fb377a685e510714dac6f8ce32263f15 Mon Sep 17 00:00:00 2001 From: freeqaz Date: Thu, 22 Oct 2026 23:48:24 +0000 Subject: [PATCH 2/3] test(repository): keep only the regression case the native contract alone refuses Review follow-up: the fractional duration and unknown option cases were already refused through the repository policy schema, so they duplicated existing coverage. --- tests/integration/configuration-startup.test.mjs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/integration/configuration-startup.test.mjs b/tests/integration/configuration-startup.test.mjs index a3e9e7fcef..70d732db7b 100644 --- a/tests/integration/configuration-startup.test.mjs +++ b/tests/integration/configuration-startup.test.mjs @@ -936,22 +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 own configuration contract (finding 2214): an existing CA file under a - // non-canonical path, a fractional duration and an unknown option are refused at load. + // 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.configuration.sessionDurationSeconds = 600.5), - nativeSenderRefusal("/sessionDurationSeconds", "INVALID_TYPE"), - ], - [ - (value) => (value.repository.configuration.unexpected = true), - nativeSenderRefusal("", "UNKNOWN_FIELD"), - ], [(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/], From e2f50a15f3817a0fa58fabd279ffbc25e1ead6f1 Mon Sep 17 00:00:00 2001 From: freeqaz Date: Fri, 23 Oct 2026 00:06:31 +0000 Subject: [PATCH 3/3] fix(repository): check the native contract once, outside the credential boundary The repository credential source boundary allows no new runtime contracts import in the GitHub installation parser. Drop its discarded validateConfiguration call; composition already validates the same merged native configuration with requireConfiguration. --- .../src/composition/repository-credentials/platform.ts | 6 ++++-- .../controller/src/drivers/repo/github/installation.ts | 10 +--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/controller/src/composition/repository-credentials/platform.ts b/apps/controller/src/composition/repository-credentials/platform.ts index 3c724a53b0..ac39d311aa 100644 --- a/apps/controller/src/composition/repository-credentials/platform.ts +++ b/apps/controller/src/composition/repository-credentials/platform.ts @@ -45,8 +45,10 @@ 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 contract; it is not a Repo Driver. Both contracts - // refuse at load, before any Backend is built (finding 2214). + // 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, diff --git a/apps/controller/src/drivers/repo/github/installation.ts b/apps/controller/src/drivers/repo/github/installation.ts index 3247eabbb4..bfbeecab7b 100644 --- a/apps/controller/src/drivers/repo/github/installation.ts +++ b/apps/controller/src/drivers/repo/github/installation.ts @@ -1,9 +1,6 @@ import { createHash } from "node:crypto"; import { SandboxRepositoryRecipientUnverifiedError } from "@openclaw-enterprise/occ"; -import { - requireConfiguration, - type GitHubRepositoryCredentialBackendDefinition, -} from "@openclaw-enterprise/contracts"; +import type { GitHubRepositoryCredentialBackendDefinition } from "@openclaw-enterprise/contracts"; import { closed, nonempty, @@ -12,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"; @@ -38,10 +34,6 @@ export function githubRepositoryConfiguration(input: unknown, mode: "development controlSocket: nonempty(native.controlSocket, "repository.nativeSender.controlSocket"), publicCaPath: nonempty(native.publicCaPath, "repository.nativeSender.publicCaPath"), }; - requireConfiguration(GitHubNativeRepositorySender, { - ...object(selection.configuration, "repository.configuration"), - ...nativeSender, - }); } return { selection,