Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
101 changes: 101 additions & 0 deletions src/lib/onboard/managed-startup-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,107 @@ describe("managed startup profile", () => {
).toThrow(/credential-shaped field name/);
});

it("accepts schema-owned messaging package pins and credential placeholder lines (#9355)", () => {
expect(() =>
validateManagedStartupProfile({
...OPENCLAW_PROFILE,
messaging: {
plan: {
...OPENCLAW_PROFILE.messaging.plan,
buildSteps: [
{
channelId: "slack",
kind: "package-install",
outputId: "slack-openclaw-plugin",
required: true,
value: {
manager: "npm",
spec: "@slack/web-api@7.9.3",
pin: true,
},
},
],
agentRender: [
...OPENCLAW_PROFILE.messaging.plan.agentRender,
{
channelId: "slack",
agent: "hermes",
target: "~/.hermes/.env",
kind: "env-lines",
lines: [
"SLACK_BOT_TOKEN=xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN",
"DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN",
],
templateRefs: ["credential.slackBotToken.placeholder"],
},
],
},
},
}),
).not.toThrow();
});

it.each([
["a raw credential", `SLACK_BOT_TOKEN=xoxb-${"a".repeat(32)}`],
["a malformed assignment", "SLACK_BOT_TOKEN =openshell:resolve:env:SLACK_BOT_TOKEN"],
[
"a placeholder for a different environment key",
"SLACK_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN",
],
])("rejects %s in messaging environment lines (#9355)", (_label, line) => {
expect(() =>
validateManagedStartupProfile({
...OPENCLAW_PROFILE,
messaging: {
plan: {
...OPENCLAW_PROFILE.messaging.plan,
agentRender: [
{
channelId: "slack",
agent: "hermes",
target: "~/.hermes/.env",
kind: "env-lines",
lines: [line],
templateRefs: ["credential.slackBotToken.placeholder"],
},
],
},
},
}),
).toThrow(/credential-shaped string data/);
});

it.each([
[
"a package pin outside buildSteps[*].value",
{
...OPENCLAW_PROFILE.messaging.plan,
buildSteps: [{ pin: true }],
},
],
[
"a non-boolean package pin",
{
...OPENCLAW_PROFILE.messaging.plan,
buildSteps: [{ value: { pin: "true" } }],
},
],
[
"a credential placeholder assignment outside agentRender[*].lines[*]",
{
...OPENCLAW_PROFILE.messaging.plan,
note: "SLACK_BOT_TOKEN=openshell:resolve:env:SLACK_BOT_TOKEN",
},
],
])("rejects %s (#9355)", (_label, plan) => {
expect(() =>
validateManagedStartupProfile({
...OPENCLAW_PROFILE,
messaging: { plan },
}),
).toThrow(/credential-shaped/);
});

it.each([
["routed inference", "inference", "routedBaseUrl"],
["upstream inference", "inference", "upstreamEndpointUrl"],
Expand Down
64 changes: 58 additions & 6 deletions src/lib/onboard/managed-startup/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const NON_SECRET_KEY_METADATA_NAMES = new Set([
]);
const MESSAGING_CREDENTIAL_PLACEHOLDER_RE =
/^(?:openshell:resolve:env:|[A-Za-z0-9]+-OPENSHELL-RESOLVE-ENV-)(?:v[0-9]+_)?[A-Z][A-Z0-9_]*$/u;
const JSON_ARRAY_INDEX_SEGMENT_RE = /^\[(?:0|[1-9][0-9]*)\]$/u;
const SECRET_VALUE_PATTERNS: readonly RegExp[] = [
/nvapi-[A-Za-z0-9_-]{10,}/u,
/nvcf-[A-Za-z0-9_-]{10,}/u,
Expand Down Expand Up @@ -1002,6 +1003,54 @@ function isMessagingCredentialPlaceholder(path: readonly string[], value: unknow
);
}

function messagingCredentialPlaceholderEnvKey(value: string): string | null {
if (!MESSAGING_CREDENTIAL_PLACEHOLDER_RE.test(value)) return null;
const marker = value.startsWith("openshell:resolve:env:")
? "openshell:resolve:env:"
: "-OPENSHELL-RESOLVE-ENV-";
const key = value.slice(value.indexOf(marker) + marker.length);
return key.replace(/^v[0-9]+_/u, "");
}

function containsMessagingCredentialPlaceholder(value: string): boolean {
return value.includes("openshell:resolve:env:") || value.includes("-OPENSHELL-RESOLVE-ENV-");
}

function isMessagingCredentialPlaceholderAssignment(
path: readonly string[],
value: string,
): boolean {
if (
path.length !== 6 ||
path[0] !== "messaging" ||
path[1] !== "plan" ||
path[2] !== "agentRender" ||
!JSON_ARRAY_INDEX_SEGMENT_RE.test(path[3] ?? "") ||
path[4] !== "lines" ||
!JSON_ARRAY_INDEX_SEGMENT_RE.test(path[5] ?? "")
) {
return false;
}
const separator = value.indexOf("=");
if (separator <= 0 || value.indexOf("=", separator + 1) !== -1) return false;
const envKey = value.slice(0, separator);
const placeholderEnvKey = messagingCredentialPlaceholderEnvKey(value.slice(separator + 1));
return CREDENTIAL_ENV_NAME_PATTERN.test(envKey) && envKey === placeholderEnvKey;
}

function isMessagingPackagePin(path: readonly string[], value: unknown): boolean {
return (
path.length === 6 &&
path[0] === "messaging" &&
path[1] === "plan" &&
path[2] === "buildSteps" &&
JSON_ARRAY_INDEX_SEGMENT_RE.test(path[3] ?? "") &&
path[4] === "value" &&
path[5] === "pin" &&
typeof value === "boolean"
);
}

function containsUrlWithCredentialMaterial(value: string): boolean {
const candidates = value.match(URL_CANDIDATE_RE) ?? [];
for (let index = 0; index < candidates.length; index += 1) {
Expand Down Expand Up @@ -1367,7 +1416,9 @@ function assertPayloadStructureAndCredentialShapes(root: unknown): void {
observeText(current.value);
if (
!isMessagingCredentialPlaceholder(current.path, current.value) &&
valueLooksLikeSecret(current.value)
!isMessagingCredentialPlaceholderAssignment(current.path, current.value) &&
(valueLooksLikeSecret(current.value) ||
containsMessagingCredentialPlaceholder(current.value))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) {
invalid(
`payload field ${payloadPath(current.path)} contains credential-shaped string data`,
Expand Down Expand Up @@ -1450,7 +1501,11 @@ function assertPayloadStructureAndCredentialShapes(root: unknown): void {
invalid("payload must contain only JSON data properties");
}
const child = descriptor.value;
if (isCredentialShapedName(key) && !isMessagingCredentialPlaceholder(current.path, child)) {
if (
isCredentialShapedName(key) &&
!isMessagingCredentialPlaceholder(current.path, child) &&
!isMessagingPackagePin([...current.path, key], child)
) {
invalid(
`payload field ${payloadPath([...current.path, key])} has a credential-shaped field name`,
);
Expand Down Expand Up @@ -1775,10 +1830,7 @@ function validateInference(value: unknown, agent: ManagedStartupAgent): ManagedS
if (primaryModelRef !== null || compatibility !== null || inputModalities !== null) {
invalid(`${agent} does not support primaryModelRef, compatibility, or inputModalities`);
}
if (
agent === "langchain-deepagents-code" &&
!isValidDcodeUpstreamProvider(upstreamProvider)
) {
if (agent === "langchain-deepagents-code" && !isValidDcodeUpstreamProvider(upstreamProvider)) {
invalid(
"inference.upstreamProvider must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode",
);
Expand Down
Loading