Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
44 changes: 38 additions & 6 deletions src/lib/onboard/initial-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { isPortableExperimentalProfile } from "./experimental/portable-profile";
export type InitialSandboxPolicy = {
policyPath: string;
appliedPresets: string[];
/** Exact provider names referenced by credential-bound endpoints in this policy. */
credentialBindingProviders?: string[];
sourceBytes?: Buffer;
cleanup?: () => boolean;
cleanupExact?: () => boolean;
Expand Down Expand Up @@ -361,6 +363,32 @@ export function getNetworkPolicyNames(policyContent: string): Set<string> | null
}
}

function getCredentialBindingProviders(policyContent: string): string[] {
const parsed = YAML.parse(policyContent);
if (!isObjectRecord(parsed) || !isObjectRecord(parsed.network_policies)) return [];

const providers = new Set<string>();
for (const policy of Object.values(parsed.network_policies)) {
if (!isObjectRecord(policy) || !Array.isArray(policy.endpoints)) continue;
for (const endpoint of policy.endpoints) {
if (!isObjectRecord(endpoint) || endpoint.credential_binding === undefined) continue;
const binding = endpoint.credential_binding;
if (
!isObjectRecord(binding) ||
typeof binding.provider !== "string" ||
binding.provider.length === 0 ||
binding.provider.trim() !== binding.provider
) {
throw new Error(
"Cannot prepare sandbox create policy; a credential binding has no exact provider name.",
);
}
providers.add(binding.provider);
}
}
return [...providers];
}

function filterHermesInactiveMessagingPolicies(
policyContent: string,
activeMessagingChannels: string[],
Expand Down Expand Up @@ -444,12 +472,16 @@ function resolveInitialSandboxCreatePolicy(
.every(Boolean)
: undefined;
const exactCleanupResult = () => (exactCleanup ? { cleanupExact: buildExactCleanup() } : {});
const result = (appliedPresets: string[]): InitialSandboxPolicy => ({
...effectivePolicy,
appliedPresets,
cleanup: buildCleanup(),
...exactCleanupResult(),
});
const result = (appliedPresets: string[]): InitialSandboxPolicy => {
const credentialBindingProviders = getCredentialBindingProviders(basePolicy);
return {
...effectivePolicy,
appliedPresets,
...(credentialBindingProviders.length > 0 ? { credentialBindingProviders } : {}),
cleanup: buildCleanup(),
...exactCleanupResult(),
};
};
const cleanupOnError = () => {
for (const cleanup of [...cleanupFns].reverse()) {
try {
Expand Down
21 changes: 19 additions & 2 deletions src/lib/onboard/sandbox-create-intent-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
resolveSandboxCreateMessagingProviderRequests,
} from "./sandbox-create-intent";
import type { SandboxCreateIntent } from "./sandbox-create-intent-types";
import { getActiveChannelsFromPlan } from "./messaging-plan-session";
import { resolveSandboxCreatePolicyTier } from "./sandbox-create-plan";
import {
selectHermesPortableExtraProviderPlan,
Expand Down Expand Up @@ -67,6 +68,22 @@ export function createSandboxCreateIntentResolver<
return deps.filterEnabledChannelsByAgent(enabledChannels ? [...enabledChannels] : null, agent);
}

function resolveSelectedChannels(
input: Pick<
CompleteSandboxCreateIntentInput<Agent, ResourceProfile>,
"sandboxName" | "enabledChannels" | "agent"
>,
): string[] | null {
const selected = filterEnabledChannels(input.enabledChannels, input.agent);
if (selected !== null) return selected;
const stagedPlan = deps.messagingPreflightDeps.readMessagingPlanFromEnv();
if (stagedPlan?.sandboxName === input.sandboxName) {
return filterEnabledChannels(getActiveChannelsFromPlan(stagedPlan), input.agent) ?? [];
}
const agentName = input.agent?.name?.trim().toLowerCase();
return agentName && agentName !== "openclaw" ? null : [];
}

async function prepareMessagingCapabilities(
input: Pick<
CompleteSandboxCreateIntentInput<Agent, ResourceProfile>,
Expand All @@ -91,7 +108,7 @@ export function createSandboxCreateIntentResolver<
const result = await prepareSandboxMessagingPreflight(
{
channels: deps.channels,
enabledChannels: filterEnabledChannels(input.enabledChannels, input.agent),
enabledChannels: resolveSelectedChannels(input),
sandboxName: input.sandboxName,
agentName: input.agent?.name ?? "openclaw",
requireExactProviderBinding:
Expand Down Expand Up @@ -133,7 +150,7 @@ export function createSandboxCreateIntentResolver<
inferenceProvider: input.inferenceProvider,
hostLocalInferenceRouteOnly: input.hostLocalInferenceRouteOnly === true,
channels: deps.channels,
enabledChannels: filterEnabledChannels(input.enabledChannels, input.agent),
enabledChannels: resolveSelectedChannels(input),
disabledChannelNames: messaging.disabledChannelNames,
messagingProviderRequests: resolveSandboxCreateMessagingProviderRequests(
messaging.messagingTokenDefs,
Expand Down
49 changes: 30 additions & 19 deletions src/lib/onboard/sandbox-create-intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,31 @@ function filterEnabledChannelNames(

function filterMessagingProviderRequestsByEnabledChannel(
requests: readonly SandboxCreateMessagingProviderRequest[],
selectedChannelNames: ReadonlySet<string> | null,
disabledChannelNames: ReadonlySet<string>,
): SandboxCreateMessagingProviderRequest[] {
return requests.filter(({ channel }) => !channel || !disabledChannelNames.has(channel));
return requests.filter(
({ channel }) =>
!channel ||
((!selectedChannelNames || selectedChannelNames.has(channel)) &&
!disabledChannelNames.has(channel)),
);
}

function resolveTokenProviderChannelMap(
export function filterMessagingProvidersForSandboxCreate(
providerNames: readonly string[],
requests: readonly SandboxCreateMessagingProviderRequest[],
): Map<string, string> {
activeChannelNames: Iterable<string>,
disabledChannelNames: Iterable<string>,
): string[] {
const providerChannels = new Map<string, string>();
for (const { channel, name } of requests) {
if (channel) providerChannels.set(name, channel);
}
return providerChannels;
}

function filterMessagingProvidersByEnabledChannel(
providerNames: string[],
providerChannels: ReadonlyMap<string, string>,
disabledChannelNames: ReadonlySet<string>,
): string[] {
return providerNames.filter((providerName) => {
const eligibleChannels = new Set([...activeChannelNames, ...disabledChannelNames]);
return [...new Set(providerNames)].filter((providerName) => {
const channel = providerChannels.get(providerName);
return !channel || !disabledChannelNames.has(channel);
return !channel || eligibleChannels.has(channel);
});
}

Expand All @@ -64,6 +66,7 @@ function resolveActiveMessagingChannels({
| "primaryMessagingCredentialEnvKeys"
| "reusableMessagingChannels"
>): string[] {
const selectedChannelNames = enabledChannels == null ? null : new Set(enabledChannels);
const primaryCredentialEnvKeys = new Set(primaryMessagingCredentialEnvKeys);
const qrSelectedChannels = resolveQrSelectedChannels(
[...channels],
Expand All @@ -76,9 +79,15 @@ function resolveActiveMessagingChannels({
...messagingProviderRequests
.filter(({ credentialConfigured }) => credentialConfigured)
.flatMap(({ channel, envKey }) => {
return channel && primaryCredentialEnvKeys.has(envKey) ? [channel] : [];
return channel &&
(!selectedChannelNames || selectedChannelNames.has(channel)) &&
primaryCredentialEnvKeys.has(envKey)
? [channel]
: [];
}),
...reusableMessagingChannels,
...reusableMessagingChannels.filter(
(channel) => !selectedChannelNames || selectedChannelNames.has(channel),
),
...qrSelectedChannels,
]),
],
Expand Down Expand Up @@ -153,11 +162,12 @@ export function resolveSandboxCreateIntent({
policyTier,
baselineExclusions = [],
}: ResolveSandboxCreateIntentInput): SandboxCreateIntent {
const selectedChannelNames = enabledChannels == null ? null : new Set(enabledChannels);
const enabledMessagingProviderRequests = filterMessagingProviderRequestsByEnabledChannel(
messagingProviderRequests,
selectedChannelNames,
disabledChannelNames,
);
const providerChannels = resolveTokenProviderChannelMap(messagingProviderRequests);
const activeMessagingChannels = resolveActiveMessagingChannels({
channels,
disabledChannelNames,
Expand All @@ -166,9 +176,10 @@ export function resolveSandboxCreateIntent({
primaryMessagingCredentialEnvKeys,
reusableMessagingChannels,
});
const enabledReusableMessagingProviders = filterMessagingProvidersByEnabledChannel(
[...new Set(reusableMessagingProviders)],
providerChannels,
const enabledReusableMessagingProviders = filterMessagingProvidersForSandboxCreate(
reusableMessagingProviders,
messagingProviderRequests,
activeMessagingChannels,
disabledChannelNames,
);

Expand Down
111 changes: 68 additions & 43 deletions src/lib/onboard/sandbox-create-plan-materialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import type { InitialSandboxPolicy } from "./initial-policy";
import type { MessagingTokenDef } from "./messaging-prep";
import { filterMessagingProvidersForSandboxCreate } from "./sandbox-create-intent";
import type {
MaterializeSandboxCreatePlanInput,
SandboxCreateIntent,
Expand Down Expand Up @@ -145,8 +146,10 @@ export function validateSandboxCreateIntentBindings(
messagingTokenDefs: readonly MessagingTokenDef[],
): MessagingTokenDef[] {
const disabledChannelNames = new Set(intent.disabledChannelNames);
const activeChannelNames = new Set(intent.policy.activeMessagingChannels);
const enabledRequests = intent.messagingProviderRequests.filter(
({ channel }) => !channel || !disabledChannelNames.has(channel),
({ channel }) =>
!channel || (activeChannelNames.has(channel) && !disabledChannelNames.has(channel)),
);
const intentRequestKeys = new Set(
intent.messagingProviderRequests.map(messagingProviderRequestKey),
Expand Down Expand Up @@ -192,25 +195,32 @@ export function validateSandboxCreateIntentBindings(
});
}

function resolveProviderChannelMap(
requests: readonly SandboxCreateMessagingProviderRequest[],
): Map<string, string> {
const providerChannels = new Map<string, string>();
for (const { channel, name } of requests) {
if (channel) providerChannels.set(name, channel);
function assertCredentialBindingProvidersAttached(
policy: InitialSandboxPolicy,
createProviders: ReadonlySet<string>,
): void {
for (const provider of policy.credentialBindingProviders ?? []) {
if (createProviders.has(provider)) continue;
policy.cleanup?.();
throw new Error(
`Cannot create sandbox; create-time policy requires credential provider '${provider}', but the sandbox create plan does not attach it.`,
);
}
return providerChannels;
}

function filterDisabledMessagingProviders(
providerNames: string[],
providerChannels: ReadonlyMap<string, string>,
disabledChannelNames: ReadonlySet<string>,
): string[] {
return providerNames.filter((providerName) => {
const channel = providerChannels.get(providerName);
return !channel || !disabledChannelNames.has(channel);
});
function buildCreateProviderSet(
intent: SandboxCreateIntent,
messagingProviders: readonly string[],
hermesToolGatewayProvider: string | null,
): Set<string> {
return new Set(
[
intent.inferenceProvider,
...messagingProviders,
hermesToolGatewayProvider,
...intent.extraProviders,
].filter((provider): provider is string => Boolean(provider)),
);
}

/** Materialize policy, route metadata, resources, and providers from a secretless intent. */
Expand Down Expand Up @@ -246,12 +256,6 @@ export function materializeSandboxCreatePlan({
intent.gpuRoutePlan,
prepareInitialSandboxCreatePolicy,
);
try {
discloseInitialSandboxPolicy?.(initialSandboxPolicy);
} catch (error) {
initialSandboxPolicy.cleanup?.();
throw error;
}
const createArgs = [
"--from",
fromRef,
Expand All @@ -264,34 +268,55 @@ export function materializeSandboxCreatePlan({
...intent.resourceCreateArgs,
];

runProviderPreDeleteCleanup();
const providerChannels = resolveProviderChannelMap(intent.messagingProviderRequests);
const messagingProviders = filterDisabledMessagingProviders(
const hermesToolGatewayProvider =
intent.hermesToolGateways.length > 0
? getHermesToolGatewayProviderName(intent.sandboxName)
: null;
const plannedMessagingProviders = filterMessagingProvidersForSandboxCreate(
[
...new Set([
...upsertMessagingProviders(enabledMessagingTokenDefs, {
replaceExisting: true,
allowedSandboxes: [intent.sandboxName],
}),
...intent.reusableMessagingProviders,
]),
...enabledMessagingTokenDefs.filter(({ token }) => Boolean(token)).map(({ name }) => name),
...intent.reusableMessagingProviders,
],
providerChannels,
new Set(intent.disabledChannelNames),
intent.messagingProviderRequests,
intent.policy.activeMessagingChannels,
intent.disabledChannelNames,
);
const createProviders = new Set<string>();
if (intent.inferenceProvider) createProviders.add(intent.inferenceProvider);
for (const provider of messagingProviders) createProviders.add(provider);
if (intent.hermesToolGateways.length > 0) {
createProviders.add(getHermesToolGatewayProviderName(intent.sandboxName));
assertCredentialBindingProvidersAttached(
initialSandboxPolicy,
buildCreateProviderSet(intent, plannedMessagingProviders, hermesToolGatewayProvider),
);
try {
discloseInitialSandboxPolicy?.(initialSandboxPolicy);
} catch (error) {
initialSandboxPolicy.cleanup?.();
throw error;
}
for (const provider of intent.extraProviders) createProviders.add(provider);

runProviderPreDeleteCleanup();
const messagingProviders = filterMessagingProvidersForSandboxCreate(
[
...upsertMessagingProviders(enabledMessagingTokenDefs, {
replaceExisting: true,
allowedSandboxes: [intent.sandboxName],
}),
...intent.reusableMessagingProviders,
],
intent.messagingProviderRequests,
intent.policy.activeMessagingChannels,
intent.disabledChannelNames,
);
const createProviders = buildCreateProviderSet(
intent,
messagingProviders,
hermesToolGatewayProvider,
);
assertCredentialBindingProvidersAttached(initialSandboxPolicy, createProviders);
for (const provider of createProviders) {
createArgs.push("--provider", provider);
}

return {
activeMessagingChannels: [...intent.activeMessagingChannels],
activeMessagingChannels: [...intent.policy.activeMessagingChannels],
initialSandboxPolicy,
policyTier: intent.policy.options.policyTier,
createArgs,
Expand All @@ -312,7 +337,7 @@ export function materializeHermesPortableCreatePlan(input: {
intent.policy.options.agentName !== "hermes" ||
!["none", "native-only"].includes(intent.gpuRoutePlan) ||
(intent.hostMounts?.length ?? 0) > 0 ||
intent.activeMessagingChannels.length > 0 ||
intent.policy.activeMessagingChannels.length > 0 ||
intent.messagingProviderRequests.length > 0 ||
intent.reusableMessagingProviders.length > 0 ||
intent.extraProviders.length > 0 ||
Expand Down
Loading
Loading