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
47 changes: 31 additions & 16 deletions apps/controller/src/drivers/compute/kubernetes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2646,14 +2646,16 @@ function referenceSetting(value: unknown, setting: string): string | undefined {
// The startup model probe gets only the selected provider's settings, the primary model's entry
// and the binding's credential, so a reference there (other than the binding's apiKey) cannot
// resolve. Another Compute may resolve it, so Configuration save accepts it; Kubernetes admission
// names the setting, never its value (finding 2828). On Dedicated OpenClaw the native worker
// gets only the selected catalog entries, so the probe gets only the primary model's entry: no
// other entry can supply its endpoint (findings 2882, 2883).
// names the setting, never its value (finding 2828). On Dedicated OpenClaw (`nativeAuthMethod`
// set) the probe gets the primary model's entry as nativeRuntimeConfiguration renders it for the
// native worker: no other entry can supply its endpoint (findings 2882, 2883), and the probe
// resolves the same model ID and checked endpoint as the worker (finding 2903). The checked
// endpoint is the provider default, not a row setting (finding 2904).
function harnessProbeConfiguration(
configuration: OpenClawConfigurationDocument,
embedded = false,
settingError: (setting: string, requirement: string) => Error = preparationSettingError,
native = false,
nativeAuthMethod?: HarnessAuthSnapshot["method"],
): object {
const model = harnessPrimaryModel(configuration);
const { providerId, environmentName } = harnessModelAuthentication(configuration, embedded);
Expand All @@ -2678,12 +2680,22 @@ function harnessProbeConfiguration(
if (fragment !== undefined && environmentName === CUSTOM_MODEL_API_KEY) {
fragment.apiKey = { source: "env", provider: "default", id: CUSTOM_MODEL_API_KEY };
}
if (native && fragment !== undefined) {
const matches = nativeOpenAiModelSelections(configuration)?.find(
(selection) => selection.reference === model,
)?.matches;
// nativeRuntimeConfiguration refuses any other count; the probe never gets another entry.
fragment.models = matches?.length === 1 ? [matches[0]!.entry] : [];
if (nativeAuthMethod !== undefined) {
// An authored row may carry the provider prefix (openai/<id>); OpenClaw's probe then cannot
// route a known model whose row endpoint differs from the provider's, while the worker gets
// the split ID. The rendered row also makes its checked endpoint the provider default, so an
// unchecked provider baseUrl never reaches the probe.
const rendered = nativeRuntimeConfiguration(configuration, settingError, nativeAuthMethod);
const { id } = splitModelRef(model);
// The rendering holds one row for each selected model, the primary included.
const primary = rendered.models.providers.openai.models.find((row) => row.id === id)!;
if (fragment !== undefined) {
// The row inherits its endpoint from the provider, so the runtime's upfront credential
// check, which skips rows with their own baseUrl, still applies (finding 2904).
const { baseUrl, ...row } = primary;
fragment.baseUrl = baseUrl;
fragment.models = [row];
}
}
return {
agents: {
Expand Down Expand Up @@ -3049,7 +3061,7 @@ function nativeRuntimeConfiguration(
configuration: OpenClawConfigurationDocument,
createError: (setting: string, requirement: string) => Error,
authMethod: HarnessAuthSnapshot["method"],
): object {
) {
const defaults = asRecord(asRecord(configuration.agents)?.defaults);
if (asRecord(defaults?.embeddedAgent)?.runtimeServer !== undefined) {
throw createError(
Expand Down Expand Up @@ -4694,11 +4706,14 @@ export class KubernetesComputeDriver implements ComputeDriver {
"Harness authentication requires a compatible model provider.",
);
}
// On Dedicated OpenClaw this renders the native catalog too (nativeRuntimeConfiguration).
if (openclaw) {
harnessProbeConfiguration(configuration, embedded, callerSettingError, native);
}
if (native) {
nativeRuntimeConfiguration(configuration, callerSettingError, auth.method);
harnessProbeConfiguration(
configuration,
embedded,
callerSettingError,
native ? auth.method : undefined,
);
}
if (codex) {
requireCodexGatewayConfigurationShape(configuration);
Expand Down Expand Up @@ -15162,7 +15177,7 @@ chmodSync(${JSON.stringify(nodeStatePath)}, 0o700);`;
revision.configuration,
embedded,
preparationSettingError,
revision.harness.mode === "dedicated",
revision.harness.mode === "dedicated" ? revision.harnessAuth.method : undefined,
),
),
},
Expand Down
3 changes: 2 additions & 1 deletion docs/flows/harness-execution-topology.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ checks each selected model (`apps/controller/src/drivers/native-openai-catalog.t
the selection Compute renders) against the immutable source endpoint before
attachment, and a refusal names the `baseUrl` setting and both hosts. Catalog
ids match after trimming, as OpenClaw merges such entries, so a second match
fails. The startup model probe gets only the primary model's entry.
fails. The startup model probe gets only the primary model's entry as rendered
for the worker, with its endpoint moved to the provider default.
`apps/controller/src/drivers/compute/kubernetes/runtime-entrypoints.ts:NATIVE_WORKER_ENTRYPOINT`
writes capacity/isolation settings and Driver-provided `OPENCLAW_WORKSPACE_DIR`;
Compute and Sandbox supply private temporary directories.
Expand Down
17 changes: 14 additions & 3 deletions tests/conformance/kubernetes-compute.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,9 @@ test("native OpenClaw preparation and activation use only outbound Sandbox enrol
"https://models.example.test:443/inference/v1/";
nativeConfiguration.models.providers.openai.api = "openai-completions";
const [selectedEntry] = nativeConfiguration.models.providers.openai.models;
// OpenClaw also names a catalog entry by the full reference; the worker and the startup probe
// get the split model ID (finding 2903).
selectedEntry.id = "openai/team/model";
// The worker gets every selected model; the startup probe checks only the primary.
nativeConfiguration.agents.defaults.model = {
primary: "openai/team/model",
Expand Down Expand Up @@ -1847,14 +1850,22 @@ test("native OpenClaw preparation and activation use only outbound Sandbox enrol
nativeConfig.models.providers.openai.models.map(({ id }) => id),
["team/model", "team/fallback"],
);
// OpenClaw merges catalog entries whose ids match after trimming, so the startup probe gets
// only the primary model's entry, as written (findings 2882, 2883).
// The startup probe gets only the primary model's entry (OpenClaw merges entries whose ids
// match after trimming; findings 2882, 2883), as the worker gets it: the split model ID, and
// the checked endpoint as the provider default. OpenClaw refuses to probe a known model under
// the authored prefix when its endpoint differs from the provider's (finding 2903), and the
// unchecked provider baseUrl never reaches the probe. The row carries no baseUrl of its own:
// the runtime's upfront credential check skips rows that do (finding 2904).
const probeConfig = JSON.parse(
provisions[0].requirements.environment.find(
({ name }) => name === "OPENCLAW_HARNESS_PROBE_CONFIG",
).value,
);
assert.deepEqual(probeConfig.models.providers.openai.models, [selectedEntry]);
const { baseUrl: workerEndpoint, ...workerPrimary } =
nativeConfig.models.providers.openai.models[0];
assert.equal(workerEndpoint, "https://models.example.test/inference/v1");
assert.deepEqual(probeConfig.models.providers.openai.models, [workerPrimary]);
assert.equal(probeConfig.models.providers.openai.baseUrl, workerEndpoint);
// The provider's observed Pod readiness and enrolled node are independent of provisioning.
// Until the provider's Pod is Ready, the wait has no named reason.
const podPending = await prepare();
Expand Down
Loading