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
36 changes: 8 additions & 28 deletions src/lib/messaging/applier/plan-filter.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type {
MessagingChannelId,
SandboxMessagingChannelPlan,
SandboxMessagingPlan,
} from "../manifest";

export function enabledPlanChannels(plan: SandboxMessagingPlan): SandboxMessagingChannelPlan[] {
const disabled = disabledPlanChannelIds(plan);
return plan.channels.filter(
(channel) => channel.active && !channel.disabled && !disabled.has(channel.channelId),
);
}

export function enabledPlanChannelIds(plan: SandboxMessagingPlan): Set<MessagingChannelId> {
return new Set(enabledPlanChannels(plan).map((channel) => channel.channelId));
}

export function filterEnabledPlanEntries<T extends { readonly channelId: MessagingChannelId }>(
plan: SandboxMessagingPlan,
entries: readonly T[],
): T[] {
const enabled = enabledPlanChannelIds(plan);
return entries.filter((entry) => enabled.has(entry.channelId));
}

function disabledPlanChannelIds(plan: SandboxMessagingPlan): Set<MessagingChannelId> {
return new Set(plan.disabledChannels);
}
export {
type EnabledPlanChannel,
type EnabledPlanSelection,
enabledPlanChannelIds,
enabledPlanChannels,
filterEnabledPlanEntries,
normalizeMessagingChannelId,
} from "../post-agent-install-selection";
102 changes: 102 additions & 0 deletions src/lib/messaging/plan-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,108 @@ describe("parseSandboxMessagingPlan", () => {
makePlan({ channels: [makePlan().channels[0], makePlan().channels[0]] }),
),
).toBeNull();
expect(
parseSandboxMessagingPlan(
makePlan({
channels: [
makePlan().channels[0],
{ ...makePlan().channels[0], channelId: " TELEGRAM " },
],
}),
),
).toBeNull();
});

it("rejects a lone noncanonical channel id even when related ids are canonical", () => {
const source = makePlan();
expect(
parseSandboxMessagingPlan(
makePlan({
channels: [{ ...source.channels[0], channelId: " Telegram " }],
disabledChannels: ["telegram"],
credentialBindings: [
{
channelId: "telegram",
credentialId: "telegramBotToken",
sourceInput: "botToken",
providerName: "sb-telegram-bridge",
providerEnvKey: "TELEGRAM_BOT_TOKEN",
placeholder: "openshell:resolve:env:TELEGRAM_BOT_TOKEN",
credentialAvailable: true,
},
],
}),
),
).toBeNull();
});

it.each([
["disabledChannels", { disabledChannels: [" Telegram "] }],
["credentialBindings", { credentialBindings: [{ channelId: "Telegram" }] }],
[
"networkPolicy.entries",
{ networkPolicy: { presets: [], entries: [{ channelId: "Telegram" }] } },
],
["agentRender", { agentRender: [{ channelId: "Telegram" }] }],
["buildSteps", { buildSteps: [{ channelId: "Telegram" }] }],
["stateUpdates", { stateUpdates: [{ channelId: "Telegram" }] }],
["healthChecks", { healthChecks: [{ channelId: "Telegram" }] }],
[
"runtimeSetup.nodePreloads",
{
runtimeSetup: {
nodePreloads: [{ channelId: "Telegram" }],
envAliases: [],
secretScans: [],
},
},
],
[
"runtimeSetup.envAliases",
{
runtimeSetup: {
nodePreloads: [],
envAliases: [{ channelId: "Telegram" }],
secretScans: [],
},
},
],
[
"runtimeSetup.secretScans",
{
runtimeSetup: {
nodePreloads: [],
envAliases: [],
secretScans: [{ channelId: "Telegram" }],
},
},
],
])("rejects noncanonical channel references in %s", (_field, overrides) => {
expect(parseSandboxMessagingPlan({ ...makePlan(), ...overrides })).toBeNull();
});

it.each([
["input", { inputs: [{ inputId: "token", channelId: "Telegram" }] }],
["hook", { hooks: [{ channelId: "Telegram" }] }],
[
"host forward",
{
hostForward: {
channelId: "Telegram",
port: 3978,
label: "Telegram webhook",
},
},
],
])("rejects a noncanonical nested %s channel reference", (_field, channelOverrides) => {
const channel = makePlan().channels[0];
expect(
parseSandboxMessagingPlan(
makePlan({
channels: [{ ...channel, ...channelOverrides }] as SandboxMessagingPlan["channels"],
}),
),
).toBeNull();
});

it("rejects any persisted channel when supportedChannelIds: [] is passed (deny-all)", () => {
Expand Down
79 changes: 71 additions & 8 deletions src/lib/messaging/plan-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type MaybeCompactMessagingPlan,
normalizePersistedSandboxMessagingPlanShape,
} from "./persistence";
import { normalizeMessagingChannelId } from "./post-agent-install-selection";

export interface SandboxMessagingPlanParseOptions {
sandboxName?: string | null;
Expand Down Expand Up @@ -49,8 +50,17 @@ export function parseSandboxMessagingPlan(
const supported = Array.isArray(options.supportedChannelIds)
? new Set(options.supportedChannelIds)
: null;
for (const [index, channel] of value.channels.entries()) {
const normalizedChannelIds = new Set<string>();
for (const channel of value.channels) {
if (!isObjectRecord(channel) || typeof channel.channelId !== "string") return null;
const normalizedChannelId = normalizeMessagingChannelId(channel.channelId);
if (
!normalizedChannelId ||
normalizedChannelId !== channel.channelId ||
normalizedChannelIds.has(normalizedChannelId)
) {
return null;
}
if (Object.hasOwn(channel, "configured") && typeof channel.configured !== "boolean") {
return null;
}
Expand All @@ -61,23 +71,47 @@ export function parseSandboxMessagingPlan(
if (Object.hasOwn(channel, "hooks") && !Array.isArray(channel.hooks)) return null;
if (
Array.isArray(channel.inputs) &&
channel.inputs.some((input) => !isObjectRecord(input) || typeof input.inputId !== "string")
channel.inputs.some(
(input) =>
!isObjectRecord(input) ||
typeof input.inputId !== "string" ||
(Object.hasOwn(input, "channelId") && input.channelId !== normalizedChannelId),
)
) {
return null;
}
if (Array.isArray(channel.hooks) && channel.hooks.some((hook) => !isObjectRecord(hook))) {
if (
Array.isArray(channel.hooks) &&
channel.hooks.some(
(hook) =>
!isObjectRecord(hook) ||
(Object.hasOwn(hook, "channelId") && hook.channelId !== normalizedChannelId),
)
) {
return null;
}
if (supported && !supported.has(channel.channelId)) return null;
if (
value.channels.findIndex(
(candidate) => isObjectRecord(candidate) && candidate.channelId === channel.channelId,
) !== index
Object.hasOwn(channel, "hostForward") &&
isObjectRecord(channel.hostForward) &&
channel.hostForward.channelId !== normalizedChannelId
) {
return null;
}
if (supported && !supported.has(channel.channelId)) return null;
normalizedChannelIds.add(normalizedChannelId);
}
if (!value.disabledChannels.every(isCanonicalMessagingChannelId)) return null;
if (
!hasCanonicalChannelReferences(value.credentialBindings) ||
!hasCanonicalChannelReferences(value.agentRender) ||
!hasCanonicalChannelReferences(value.buildSteps) ||
!hasCanonicalChannelReferences(value.stateUpdates) ||
!hasCanonicalChannelReferences(value.healthChecks) ||
!hasCanonicalNetworkPolicyReferences(value.networkPolicy) ||
!hasCanonicalRuntimeSetupReferences(value.runtimeSetup)
) {
return null;
}
if (!value.disabledChannels.every((channelId) => typeof channelId === "string")) return null;

return cloneSandboxMessagingPlan(
normalizePersistedSandboxMessagingPlanShape(value as MaybeCompactMessagingPlan),
Expand Down Expand Up @@ -192,3 +226,32 @@ function isRuntimeSetup(value: unknown): boolean {
value.secretScans.every(isObjectRecord)
);
}

function isCanonicalMessagingChannelId(value: unknown): value is string {
return (
typeof value === "string" && value.length > 0 && normalizeMessagingChannelId(value) === value
);
}

function hasCanonicalChannelReferences(value: unknown): boolean {
return (
value === undefined ||
(Array.isArray(value) &&
value.every(
(entry) => isObjectRecord(entry) && isCanonicalMessagingChannelId(entry.channelId),
))
);
}

function hasCanonicalNetworkPolicyReferences(value: unknown): boolean {
if (!isObjectRecord(value) || !Object.hasOwn(value, "entries")) return true;
return hasCanonicalChannelReferences(value.entries);
}

function hasCanonicalRuntimeSetupReferences(value: unknown): boolean {
if (value === undefined) return true;
if (!isObjectRecord(value)) return false;
return ["nodePreloads", "envAliases", "secretScans"].every((field) =>
hasCanonicalChannelReferences(value[field]),
);
}
Loading
Loading