Skip to content

Commit fe0ffea

Browse files
committed
fix(onboard): accept schema-owned messaging plan fields
1 parent 54cb2a4 commit fe0ffea

2 files changed

Lines changed: 159 additions & 6 deletions

File tree

src/lib/onboard/managed-startup-profile.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,107 @@ describe("managed startup profile", () => {
729729
).toThrow(/credential-shaped field name/);
730730
});
731731

732+
it("accepts schema-owned messaging package pins and credential placeholder lines (#9355)", () => {
733+
expect(() =>
734+
validateManagedStartupProfile({
735+
...OPENCLAW_PROFILE,
736+
messaging: {
737+
plan: {
738+
...OPENCLAW_PROFILE.messaging.plan,
739+
buildSteps: [
740+
{
741+
channelId: "slack",
742+
kind: "package-install",
743+
outputId: "slack-openclaw-plugin",
744+
required: true,
745+
value: {
746+
manager: "npm",
747+
spec: "@slack/web-api@7.9.3",
748+
pin: true,
749+
},
750+
},
751+
],
752+
agentRender: [
753+
...OPENCLAW_PROFILE.messaging.plan.agentRender,
754+
{
755+
channelId: "slack",
756+
agent: "hermes",
757+
target: "~/.hermes/.env",
758+
kind: "env-lines",
759+
lines: [
760+
"SLACK_BOT_TOKEN=xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",
761+
"DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN",
762+
],
763+
templateRefs: ["credential.slackBotToken.placeholder"],
764+
},
765+
],
766+
},
767+
},
768+
}),
769+
).not.toThrow();
770+
});
771+
772+
it.each([
773+
["a raw credential", `SLACK_BOT_TOKEN=xoxb-${"a".repeat(32)}`],
774+
["a malformed assignment", "SLACK_BOT_TOKEN =openshell:resolve:env:SLACK_BOT_TOKEN"],
775+
[
776+
"a placeholder for a different environment key",
777+
"SLACK_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN",
778+
],
779+
])("rejects %s in messaging environment lines (#9355)", (_label, line) => {
780+
expect(() =>
781+
validateManagedStartupProfile({
782+
...OPENCLAW_PROFILE,
783+
messaging: {
784+
plan: {
785+
...OPENCLAW_PROFILE.messaging.plan,
786+
agentRender: [
787+
{
788+
channelId: "slack",
789+
agent: "hermes",
790+
target: "~/.hermes/.env",
791+
kind: "env-lines",
792+
lines: [line],
793+
templateRefs: ["credential.slackBotToken.placeholder"],
794+
},
795+
],
796+
},
797+
},
798+
}),
799+
).toThrow(/credential-shaped string data/);
800+
});
801+
802+
it.each([
803+
[
804+
"a package pin outside buildSteps[*].value",
805+
{
806+
...OPENCLAW_PROFILE.messaging.plan,
807+
buildSteps: [{ pin: true }],
808+
},
809+
],
810+
[
811+
"a non-boolean package pin",
812+
{
813+
...OPENCLAW_PROFILE.messaging.plan,
814+
buildSteps: [{ value: { pin: "true" } }],
815+
},
816+
],
817+
[
818+
"a credential placeholder assignment outside agentRender[*].lines[*]",
819+
{
820+
...OPENCLAW_PROFILE.messaging.plan,
821+
note: "SLACK_BOT_TOKEN=openshell:resolve:env:SLACK_BOT_TOKEN",
822+
},
823+
],
824+
])("rejects %s (#9355)", (_label, plan) => {
825+
expect(() =>
826+
validateManagedStartupProfile({
827+
...OPENCLAW_PROFILE,
828+
messaging: { plan },
829+
}),
830+
).toThrow(/credential-shaped/);
831+
});
832+
732833
it.each([
733834
["routed inference", "inference", "routedBaseUrl"],
734835
["upstream inference", "inference", "upstreamEndpointUrl"],

src/lib/onboard/managed-startup/profile.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const NON_SECRET_KEY_METADATA_NAMES = new Set([
5959
]);
6060
const MESSAGING_CREDENTIAL_PLACEHOLDER_RE =
6161
/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:v[0-9]+_)?[A-Z][A-Z0-9_]*$/u;
62+
const JSON_ARRAY_INDEX_SEGMENT_RE = /^\[(?:0|[1-9][0-9]*)\]$/u;
6263
const SECRET_VALUE_PATTERNS: readonly RegExp[] = [
6364
/nvapi-[A-Za-z0-9_-]{10,}/u,
6465
/nvcf-[A-Za-z0-9_-]{10,}/u,
@@ -1002,6 +1003,54 @@ function isMessagingCredentialPlaceholder(path: readonly string[], value: unknow
10021003
);
10031004
}
10041005

1006+
function messagingCredentialPlaceholderEnvKey(value: string): string | null {
1007+
if (!MESSAGING_CREDENTIAL_PLACEHOLDER_RE.test(value)) return null;
1008+
const marker = value.startsWith("openshell:resolve:env:")
1009+
? "openshell:resolve:env:"
1010+
: "-OPENSHELL-RESOLVE-ENV-";
1011+
const key = value.slice(value.indexOf(marker) + marker.length);
1012+
return key.replace(/^v[0-9]+_/u, "");
1013+
}
1014+
1015+
function containsMessagingCredentialPlaceholder(value: string): boolean {
1016+
return value.includes("openshell:resolve:env:") || value.includes("-OPENSHELL-RESOLVE-ENV-");
1017+
}
1018+
1019+
function isMessagingCredentialPlaceholderAssignment(
1020+
path: readonly string[],
1021+
value: string,
1022+
): boolean {
1023+
if (
1024+
path.length !== 6 ||
1025+
path[0] !== "messaging" ||
1026+
path[1] !== "plan" ||
1027+
path[2] !== "agentRender" ||
1028+
!JSON_ARRAY_INDEX_SEGMENT_RE.test(path[3] ?? "") ||
1029+
path[4] !== "lines" ||
1030+
!JSON_ARRAY_INDEX_SEGMENT_RE.test(path[5] ?? "")
1031+
) {
1032+
return false;
1033+
}
1034+
const separator = value.indexOf("=");
1035+
if (separator <= 0 || value.indexOf("=", separator + 1) !== -1) return false;
1036+
const envKey = value.slice(0, separator);
1037+
const placeholderEnvKey = messagingCredentialPlaceholderEnvKey(value.slice(separator + 1));
1038+
return CREDENTIAL_ENV_NAME_PATTERN.test(envKey) && envKey === placeholderEnvKey;
1039+
}
1040+
1041+
function isMessagingPackagePin(path: readonly string[], value: unknown): boolean {
1042+
return (
1043+
path.length === 6 &&
1044+
path[0] === "messaging" &&
1045+
path[1] === "plan" &&
1046+
path[2] === "buildSteps" &&
1047+
JSON_ARRAY_INDEX_SEGMENT_RE.test(path[3] ?? "") &&
1048+
path[4] === "value" &&
1049+
path[5] === "pin" &&
1050+
typeof value === "boolean"
1051+
);
1052+
}
1053+
10051054
function containsUrlWithCredentialMaterial(value: string): boolean {
10061055
const candidates = value.match(URL_CANDIDATE_RE) ?? [];
10071056
for (let index = 0; index < candidates.length; index += 1) {
@@ -1367,7 +1416,9 @@ function assertPayloadStructureAndCredentialShapes(root: unknown): void {
13671416
observeText(current.value);
13681417
if (
13691418
!isMessagingCredentialPlaceholder(current.path, current.value) &&
1370-
valueLooksLikeSecret(current.value)
1419+
!isMessagingCredentialPlaceholderAssignment(current.path, current.value) &&
1420+
(valueLooksLikeSecret(current.value) ||
1421+
containsMessagingCredentialPlaceholder(current.value))
13711422
) {
13721423
invalid(
13731424
`payload field ${payloadPath(current.path)} contains credential-shaped string data`,
@@ -1450,7 +1501,11 @@ function assertPayloadStructureAndCredentialShapes(root: unknown): void {
14501501
invalid("payload must contain only JSON data properties");
14511502
}
14521503
const child = descriptor.value;
1453-
if (isCredentialShapedName(key) && !isMessagingCredentialPlaceholder(current.path, child)) {
1504+
if (
1505+
isCredentialShapedName(key) &&
1506+
!isMessagingCredentialPlaceholder(current.path, child) &&
1507+
!isMessagingPackagePin([...current.path, key], child)
1508+
) {
14541509
invalid(
14551510
`payload field ${payloadPath([...current.path, key])} has a credential-shaped field name`,
14561511
);
@@ -1775,10 +1830,7 @@ function validateInference(value: unknown, agent: ManagedStartupAgent): ManagedS
17751830
if (primaryModelRef !== null || compatibility !== null || inputModalities !== null) {
17761831
invalid(`${agent} does not support primaryModelRef, compatibility, or inputModalities`);
17771832
}
1778-
if (
1779-
agent === "langchain-deepagents-code" &&
1780-
!isValidDcodeUpstreamProvider(upstreamProvider)
1781-
) {
1833+
if (agent === "langchain-deepagents-code" && !isValidDcodeUpstreamProvider(upstreamProvider)) {
17821834
invalid(
17831835
"inference.upstreamProvider must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode",
17841836
);

0 commit comments

Comments
 (0)