Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bb83755
feat(messaging): add hook scaffold and channel manifests
sandl99 May 22, 2026
7f79ccf
fix(messaging): allow shared hook output values
sandl99 May 22, 2026
3a92503
feat(messaging): declare enrollment hooks
sandl99 May 22, 2026
b8aa2e6
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 22, 2026
ab94308
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 22, 2026
c044b80
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 23, 2026
ab564a0
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 23, 2026
1e64396
Merge remote-tracking branch 'origin/main' into HEAD
sandl99 May 25, 2026
b96f5af
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 25, 2026
51e9591
feat(messaging): add manifest compiler (#4069)
sandl99 May 25, 2026
d8947ec
fix(messaging): address review feedback
sandl99 May 25, 2026
b030f4a
fix(messaging): inline merge key guard
sandl99 May 25, 2026
710e793
fix(messaging): validate merge subtrees
sandl99 May 25, 2026
458b885
fix(messaging): implement channel hooks and split applier
sandl99 May 25, 2026
4e78355
refactor(messaging): move policy keys into manifests
sandl99 May 25, 2026
4b0b67f
fix(messaging): include policy keys in applier context
sandl99 May 25, 2026
34fcf23
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 25, 2026
e2e31dc
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 25, 2026
7e6464d
fix(messaging): harden channel enrollment and build paths
cv May 25, 2026
ba7c0d5
fix(messaging): complete channel manifest hardening
cv May 25, 2026
55c4b57
fix(messaging): validate agent config render targets
cv May 25, 2026
812defe
fix(messaging): keep wechat hook dependencies injected
cv May 25, 2026
b602fc7
fix(messaging): keep token paste dependencies injected
cv May 25, 2026
accf1e3
refactor(messaging): unify validation hook phase
sandl99 May 26, 2026
5953299
revert(messaging): restore separate validation phases
sandl99 May 26, 2026
352855b
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 26, 2026
bb1cb78
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 26, 2026
4ebd51b
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 26, 2026
c087d21
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 27, 2026
5daef1c
test(messaging): fix Hermes env helper calls
sandl99 May 27, 2026
31dc207
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 28, 2026
51fd119
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 28, 2026
9807d4b
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 29, 2026
498e9eb
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 May 29, 2026
47de166
refactor(messaging): simplify workflow planner
sandl99 May 29, 2026
7d05b57
refactor(messaging): simplify channel compiler context
sandl99 May 29, 2026
0f2d824
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
cv May 30, 2026
52ac6fb
Merge branch 'main' into u/sdang/messaging-hooks-channels-3993-3994
sandl99 Jun 1, 2026
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
605 changes: 605 additions & 0 deletions src/lib/messaging/applier/agent-config.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/lib/messaging/applier/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export * from "./setup-applier";
export * from "./agent-config";
export * from "./openshell-provider";
export * from "./policy";
export type * from "./types";
154 changes: 154 additions & 0 deletions src/lib/messaging/applier/openshell-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { redact } from "../../security/redact";
import type {
SandboxMessagingCredentialBindingPlan,
SandboxMessagingPlan,
} from "../manifest";
import type {
MessagingCredentialApplyOptions,
MessagingCredentialApplyResult,
MessagingOpenShellRunner,
} from "./types";
import { filterEnabledPlanEntries } from "./plan-filter";

type MessagingCredentialApplyEntry = MessagingCredentialApplyResult["upserted"][number];
type MessagingCredentialReuseEntry = MessagingCredentialApplyResult["reused"][number];
type MessagingMissingCredentialEntry = MessagingCredentialApplyResult["missing"][number];
type MessagingCredentialBindingLike = Pick<
SandboxMessagingCredentialBindingPlan,
"channelId" | "credentialId" | "providerName" | "providerEnvKey"
>;

export function applyCredentialsAtOpenShell(
plan: SandboxMessagingPlan,
options: MessagingCredentialApplyOptions,
): MessagingCredentialApplyResult {
const env = options.env ?? process.env;
const runOpenshell = options.runOpenshell;
const upserted: MessagingCredentialApplyEntry[] = [];
const reused: MessagingCredentialReuseEntry[] = [];
const missing: MessagingMissingCredentialEntry[] = [];

for (const binding of filterEnabledPlanEntries(plan, plan.credentialBindings)) {
const credential = readCredentialEnv(env, binding.providerEnvKey);
if (!credential) {
if (providerExistsInGateway(binding.providerName, runOpenshell)) {
reused.push(toReuseEntry(binding));
} else {
missing.push(toMissingEntry(binding));
}
continue;
}

const action = providerExistsInGateway(binding.providerName, runOpenshell)
? "update"
: "create";
const result = runOpenshell(
buildProviderArgs(action, binding.providerName, binding.providerEnvKey),
{
ignoreError: true,
env: { [binding.providerEnvKey]: credential },
stdio: ["ignore", "pipe", "pipe"],
},
);
const status = result.status ?? 0;
if (status !== 0) {
throw new Error(
`Failed to ${action} messaging provider '${binding.providerName}': ${compactOutput(result)}`,
);
}
upserted.push({
channelId: binding.channelId,
credentialId: binding.credentialId,
providerName: binding.providerName,
envKey: binding.providerEnvKey,
action,
});
}

const providerNames = uniqueStrings([
...upserted.map((entry) => entry.providerName),
...reused.map((entry) => entry.providerName),
]);

return {
upserted,
reused,
missing,
providerNames,
sandboxCreateProviderArgs: providerNames.flatMap((providerName) => [
"--provider",
providerName,
]),
};
}

function readCredentialEnv(env: NodeJS.ProcessEnv, envKey: string): string | null {
const raw = env[envKey];
if (typeof raw !== "string") return null;
const normalized = raw.replace(/\r/g, "").trim();
return normalized || null;
}

function providerExistsInGateway(
providerName: string,
runOpenshell: MessagingOpenShellRunner,
): boolean {
const result = runOpenshell(["provider", "get", providerName], {
ignoreError: true,
stdio: ["ignore", "ignore", "ignore"],
});
return (result.status ?? 0) === 0;
}

function buildProviderArgs(
action: "create" | "update",
providerName: string,
credentialEnv: string,
): string[] {
return action === "create"
? [
"provider",
"create",
"--name",
providerName,
"--type",
"generic",
"--credential",
credentialEnv,
]
: ["provider", "update", providerName, "--credential", credentialEnv];
}

function toReuseEntry(binding: MessagingCredentialBindingLike): MessagingCredentialReuseEntry {
return {
channelId: binding.channelId,
credentialId: binding.credentialId,
providerName: binding.providerName,
envKey: binding.providerEnvKey,
};
}

function toMissingEntry(
binding: MessagingCredentialBindingLike,
): MessagingMissingCredentialEntry {
return {
channelId: binding.channelId,
credentialId: binding.credentialId,
providerName: binding.providerName,
envKey: binding.providerEnvKey,
};
}

function compactOutput(result: { readonly stdout?: unknown; readonly stderr?: unknown }): string {
const output = redact(`${String(result.stderr ?? "")}${String(result.stdout ?? "")}`)
.replace(/\r/g, "")
.trim();
return output || "OpenShell command failed.";
}

function uniqueStrings(values: readonly string[]): string[] {
return [...new Set(values.filter(Boolean))];
}
34 changes: 34 additions & 0 deletions src/lib/messaging/applier/plan-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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);
}
36 changes: 36 additions & 0 deletions src/lib/messaging/applier/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { SandboxMessagingPlan } from "../manifest";
import { filterEnabledPlanEntries } from "./plan-filter";
import type { MessagingPolicyApplyOptions, MessagingPolicyApplyResult } from "./types";

export function applyPolicyAtOpenShell(
plan: SandboxMessagingPlan,
options: MessagingPolicyApplyOptions,
): MessagingPolicyApplyResult {
const activeEntries = filterEnabledPlanEntries(plan, plan.networkPolicy.entries);
const activePresets = uniqueStrings(activeEntries.map((entry) => entry.presetName));
const activePolicyKeys = uniqueStrings(
activeEntries.flatMap((entry) => entry.policyKeys),
);
if (
activePresets.length > 0 &&
!options.applyPresets(plan.sandboxName, activePresets, {
agent: plan.agent,
entries: activeEntries,
policyKeys: activePolicyKeys,
})
) {
throw new Error(`Failed to apply messaging policy preset(s): ${activePresets.join(", ")}`);
}

return {
appliedPresets: activePresets,
appliedPolicyKeys: activePolicyKeys,
};
}

function uniqueStrings(values: readonly string[]): string[] {
return [...new Set(values.filter(Boolean))];
}
Loading