Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 4 additions & 8 deletions src/lib/onboard/policy-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,21 +253,17 @@ export function computeSetupPresetSuggestions(
const activeMessagingPresets = Array.isArray(enabledChannels)
? new Set(allMessagingChannelPolicyPresets(enabledChannels))
: null;
const hermesAgent = typeof agent === "string" && agent.trim().toLowerCase() === "hermes";
const supportOptions = { webSearchSupported: options.webSearchSupported };
const suggestions = deps.tiers
.resolveTierPresets(tierName)
.map((preset) => preset.name)
.filter((name) => setupPolicyPresetAppliesToAgent(name, agent))
// Hermes Discord egress names a sandbox-scoped credential provider. An
// open tier may contain the preset, but OpenShell rejects it unless the
// channel is active and its provider is attached to the sandbox.
// Discord egress names a sandbox-scoped credential provider. An open tier
// may contain the preset, but OpenShell rejects it unless the channel is
// active and its provider is attached to the sandbox.
.filter(
(name) =>
!hermesAgent ||
name !== "discord" ||
activeMessagingPresets === null ||
activeMessagingPresets.has(name),
name !== "discord" || activeMessagingPresets === null || activeMessagingPresets.has(name),
)
.filter(
(name) =>
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
Loading
Loading