Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
6aa13c8
feat(onboard): use OpenShell providers for messaging credentials
ericksoa Mar 30, 2026
451dc57
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Mar 30, 2026
d898544
fix(security): verify messaging providers exist after sandbox creation
ericksoa Mar 30, 2026
499053d
fix(security): test provider creation and sandbox attachment for mess…
ericksoa Mar 30, 2026
9d4d5e1
fix(security): address CodeRabbit review findings
ericksoa Mar 30, 2026
752fc94
fix(security): update credential-exposure test for blocklist pattern
ericksoa Mar 30, 2026
3fa1cf6
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Mar 30, 2026
00c267f
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Mar 31, 2026
6d23a16
fix(onboard): address review feedback for messaging credential providers
ericksoa Mar 31, 2026
3cfa41e
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Mar 31, 2026
1824df4
test(onboard): add unit tests for provider and utility functions
ericksoa Mar 31, 2026
fa571a4
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Mar 31, 2026
34f1587
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Mar 31, 2026
030fb54
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Mar 31, 2026
a5ed2d3
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Mar 31, 2026
1babf82
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Mar 31, 2026
209aecd
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Mar 31, 2026
8efb2c0
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Mar 31, 2026
230f508
fix(security): address remaining CodeRabbit review feedback on PR #1081
ericksoa Apr 1, 2026
503abf5
Merge branch 'feat/messaging-credential-providers' of https://github.…
ericksoa Apr 1, 2026
35f2c10
Merge branch 'main' into feat/messaging-credential-providers
cv Apr 1, 2026
c7a2643
Merge branch 'main' into feat/messaging-credential-providers
cv Apr 1, 2026
5e1799e
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Apr 2, 2026
c497dc3
Merge branch 'main' into feat/messaging-credential-providers
cv Apr 2, 2026
8cb7d9a
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Apr 2, 2026
d14e405
fix: check upsertProvider result and skip recreation when providers e…
ericksoa Apr 2, 2026
6dd38a5
fix: upgrade Telegram provider to full L7 URL-path credential injection
ericksoa Apr 2, 2026
b57fb18
feat: replace host-side telegram bridge with native OpenClaw channels
ericksoa Apr 2, 2026
8b6a06e
fix: normalize messaging tokens and derive credential blocklist
ericksoa Apr 2, 2026
dc2b83c
test: pin OpenShell install to v0.0.20 to isolate e2e hang
ericksoa Apr 2, 2026
fd54399
fix: prevent forward start from hanging on piped stdio inheritance
ericksoa Apr 2, 2026
90cd28c
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Apr 2, 2026
92546cc
fix: add stderr capture, verification, and retry to dashboard forward
ericksoa Apr 2, 2026
acf0db3
Merge remote-tracking branch 'origin/main' into feat/messaging-creden…
ericksoa Apr 2, 2026
c521f75
fix: remove void-return checks on ensureDashboardForward after merge
ericksoa Apr 2, 2026
2058c9f
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Apr 2, 2026
b1b616d
merge: integrate main into feat/messaging-credential-providers
ericksoa Apr 3, 2026
088cef8
fix(security): address coderabbit review feedback on messaging providers
ericksoa Apr 3, 2026
6c6818f
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Apr 3, 2026
d5c275b
test(e2e): add messaging credential provider E2E test
ericksoa Apr 3, 2026
4393df6
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Apr 3, 2026
ce95b90
ci: add messaging-providers to e2e-brev workflow dispatch options
ericksoa Apr 3, 2026
ac1c36b
Merge branch 'feat/messaging-credential-providers' of https://github.…
ericksoa Apr 3, 2026
aa8d056
ci: add messaging-providers-e2e to nightly workflow
ericksoa Apr 3, 2026
3a328d8
fix(e2e): run install.sh in messaging providers test for bare runners
ericksoa Apr 3, 2026
aa8b625
fix(security): revert non-root fail-fast in configure_messaging_channels
ericksoa Apr 3, 2026
457394f
fix(e2e): handle non-root sandbox and Node.js warnings in messaging test
ericksoa Apr 3, 2026
c140754
fix(e2e): accept Telegram 404 as valid proxy rewrite proof
ericksoa Apr 3, 2026
25a2dd9
Merge branch 'main' into feat/messaging-credential-providers
cv Apr 3, 2026
fc1dba2
merge: integrate main into feat/messaging-credential-providers
ericksoa Apr 3, 2026
07c10ca
Merge branch 'feat/messaging-credential-providers' of https://github.…
ericksoa Apr 3, 2026
2123c05
fix: remove stale telegram-bridge spawn from services.ts
ericksoa Apr 3, 2026
e76e8b9
Merge branch 'main' into feat/messaging-credential-providers
ericksoa Apr 3, 2026
3fc08b8
feat: prompt for messaging channel tokens during interactive onboard
ericksoa Apr 3, 2026
c540958
Merge branch 'feat/messaging-credential-providers' of https://github.…
ericksoa Apr 3, 2026
c50c3b4
fix: prompt for each messaging token individually during onboard
ericksoa Apr 3, 2026
21acf9a
fix: ask which messaging channels first, then prompt only those tokens
ericksoa Apr 3, 2026
ca5f607
fix: prompt for each messaging token separately with setup instructions
ericksoa Apr 3, 2026
fd12dff
feat: make messaging channels its own onboard step (6 of 8)
ericksoa Apr 3, 2026
cf23928
fix: use preset-style selection UX for messaging channels
ericksoa Apr 3, 2026
c9d6f33
fix: move messaging channels to step 5, before sandbox creation
ericksoa Apr 3, 2026
17d1a1f
fix: match policy preset UX for messaging channel selection
ericksoa Apr 3, 2026
638851b
fix: use exact Y/n/list pattern from policy presets for messaging
ericksoa Apr 3, 2026
b1de2c9
Revert "fix: use exact Y/n/list pattern from policy presets for messa…
ericksoa Apr 3, 2026
163ba05
fix: replace comma-separated messaging selector with numbered toggle
ericksoa Apr 4, 2026
3275015
Merge branch 'main' into feat/messaging-credential-providers
cv Apr 4, 2026
92e6807
fix: use single-keypress toggle for messaging channel selection
ericksoa Apr 4, 2026
0e67dbf
fix: redraw messaging toggle list in place with ANSI escape codes
ericksoa Apr 4, 2026
f768095
Merge branch 'main' into feat/messaging-credential-providers
cv Apr 4, 2026
ee014e8
fix: clear screen below cursor on redraw to prevent prompt duplication
ericksoa Apr 4, 2026
af8e06a
merge: integrate main into feat/messaging-credential-providers
ericksoa Apr 4, 2026
ac60cc7
fix(onboard): bake messaging channels into openclaw.json at build time
ericksoa Apr 5, 2026
ea66a75
merge: integrate main into feat/messaging-credential-providers
ericksoa Apr 5, 2026
366c518
fix(start): remove dead runtime openclaw.json patching from configure…
ericksoa Apr 5, 2026
e29bd43
fix(start): allow CLI clients in auto-pair watcher
ericksoa Apr 5, 2026
68de9fa
feat(onboard): collect Telegram user ID for DM allowlisting
ericksoa Apr 5, 2026
04dc48d
test(e2e): verify DM allowlisting in messaging providers test
ericksoa Apr 5, 2026
de910ea
fix(start): add explicit return 0 to configure_messaging_channels
ericksoa Apr 5, 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
112 changes: 98 additions & 14 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,15 @@ async function promptValidationRecovery(label, recovery, credentialEnv = null, h
return "selection";
}

/**
* Build the argument array for an `openshell provider create` or `update` command.
* @param {"create"|"update"} action - Whether to create or update.
* @param {string} name - Provider name.
* @param {string} type - Provider type (e.g. "openai", "anthropic", "generic").
* @param {string} credentialEnv - Credential environment variable name.
* @param {string|null} baseUrl - Optional base URL for API-compatible endpoints.
* @returns {string[]} Argument array for runOpenshell().
*/
function buildProviderArgs(action, name, type, credentialEnv, baseUrl) {
const args =
action === "create"
Expand All @@ -857,6 +866,18 @@ function buildProviderArgs(action, name, type, credentialEnv, baseUrl) {
return args;
}

/**
* Create or update an OpenShell provider in the gateway.
*
* Attempts `openshell provider create`; if that fails (provider already exists),
* falls back to `openshell provider update` with the same credential.
* @param {string} name - Provider name (e.g. "discord-bridge", "inference").
* @param {string} type - Provider type ("openai", "anthropic", "generic").
* @param {string} credentialEnv - Environment variable name for the credential.
* @param {string|null} baseUrl - Optional base URL for the provider endpoint.
* @param {Record<string, string>} [env={}] - Environment variables for the openshell command.
* @returns {{ ok: boolean, status?: number, message?: string }}
*/
function upsertProvider(name, type, credentialEnv, baseUrl, env = {}) {
const createArgs = buildProviderArgs("create", name, type, credentialEnv, baseUrl);
const runOpts = { ignoreError: true, env, stdio: ["ignore", "pipe", "pipe"] };
Expand All @@ -882,6 +903,20 @@ function upsertProvider(name, type, credentialEnv, baseUrl, env = {}) {
return { ok: true };
}

/**
* Check whether an OpenShell provider exists in the gateway.
*
* Queries the gateway-level provider registry via `openshell provider get`.
* Does NOT verify that the provider is attached to a specific sandbox —
* OpenShell CLI does not currently expose a sandbox-scoped provider query.
* @param {string} name - Provider name to look up (e.g. "discord-bridge").
* @returns {boolean} True if the provider exists in the gateway.
*/
function providerExistsInGateway(name) {
const result = runOpenshell(["provider", "get", name], { ignoreError: true });
return result.status === 0;
}

function verifyInferenceRoute(_provider, _model) {
const output = runCaptureOpenshell(["inference", "get"], { ignoreError: true });
if (!output || /Gateway inference:\s*[\r\n]+\s*Not configured/i.test(output)) {
Expand Down Expand Up @@ -2178,26 +2213,56 @@ async function createSandbox(gpu, model, provider, preferredInferenceApi = null,
];
// --gpu is intentionally omitted. See comment in startGateway().

console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`);
const chatUiUrl = process.env.CHAT_UI_URL || "http://127.0.0.1:18789";
patchStagedDockerfile(stagedDockerfile, model, chatUiUrl, String(Date.now()), provider, preferredInferenceApi);
// Only pass non-sensitive env vars to the sandbox. NVIDIA_API_KEY is NOT
// needed inside the sandbox — inference is proxied through the OpenShell
// gateway which injects the stored credential server-side. The gateway
// also strips any Authorization headers sent by the sandbox client.
// See: crates/openshell-sandbox/src/proxy.rs (header stripping),
// crates/openshell-router/src/backend.rs (server-side auth injection).
const envArgs = [formatEnvAssignment("CHAT_UI_URL", chatUiUrl)];
const sandboxEnv = { ...process.env };
delete sandboxEnv.NVIDIA_API_KEY;
// Create OpenShell providers for messaging credentials so they flow through
// the provider/placeholder system instead of raw env vars. The L7 proxy
// rewrites Authorization headers (Bearer/Bot) with real secrets at egress.
// Telegram provider is created for credential storage but the host-side bridge
// still reads from host env — Telegram uses URL-path auth (/bot{TOKEN}/) which
// the proxy can't rewrite yet.
const messagingProviders = [];
const discordToken = getCredential("DISCORD_BOT_TOKEN") || process.env.DISCORD_BOT_TOKEN;
if (discordToken) {
sandboxEnv.DISCORD_BOT_TOKEN = discordToken;
upsertProvider("discord-bridge", "generic", "DISCORD_BOT_TOKEN", null, { DISCORD_BOT_TOKEN: discordToken });
messagingProviders.push("discord-bridge");
}
const slackToken = getCredential("SLACK_BOT_TOKEN") || process.env.SLACK_BOT_TOKEN;
if (slackToken) {
sandboxEnv.SLACK_BOT_TOKEN = slackToken;
upsertProvider("slack-bridge", "generic", "SLACK_BOT_TOKEN", null, { SLACK_BOT_TOKEN: slackToken });
messagingProviders.push("slack-bridge");
}
const telegramToken = hydrateCredentialEnv("TELEGRAM_BOT_TOKEN") || process.env.TELEGRAM_BOT_TOKEN;
if (telegramToken) {
upsertProvider("telegram-bridge", "generic", "TELEGRAM_BOT_TOKEN", null, { TELEGRAM_BOT_TOKEN: telegramToken });
messagingProviders.push("telegram-bridge");
}
for (const p of messagingProviders) {
createArgs.push("--provider", p);
}

console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`);
const chatUiUrl = process.env.CHAT_UI_URL || "http://127.0.0.1:18789";
patchStagedDockerfile(stagedDockerfile, model, chatUiUrl, String(Date.now()), provider, preferredInferenceApi);
// Only pass non-sensitive env vars to the sandbox. Credentials flow through
// OpenShell providers — the gateway injects them as placeholders and the L7
// proxy rewrites Authorization headers with real secrets at egress.
// See: crates/openshell-sandbox/src/secrets.rs (placeholder rewriting),
// crates/openshell-router/src/backend.rs (inference auth injection).
const envArgs = [formatEnvAssignment("CHAT_UI_URL", chatUiUrl)];
const blockedSandboxEnvNames = new Set([
"NVIDIA_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GEMINI_API_KEY",
"BEDROCK_API_KEY",
"COMPATIBLE_API_KEY",
"COMPATIBLE_ANTHROPIC_API_KEY",
"DISCORD_BOT_TOKEN",
"SLACK_BOT_TOKEN",
"TELEGRAM_BOT_TOKEN",
]);
const sandboxEnv = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !blockedSandboxEnvNames.has(name))
);
// Run without piping through awk — the pipe masked non-zero exit codes
// from openshell because bash returns the status of the last pipeline
// command (awk, always 0) unless pipefail is set. Removing the pipe
Expand Down Expand Up @@ -2299,6 +2364,16 @@ async function createSandbox(gpu, model, provider, preferredInferenceApi = null,
console.log(" Setting up sandbox DNS proxy...");
run(`bash "${path.join(SCRIPTS, "setup-dns-proxy.sh")}" ${GATEWAY_NAME} "${sandboxName}" 2>&1 || true`, { ignoreError: true });

// Check that messaging providers exist in the gateway (sandbox attachment
// cannot be verified via CLI yet — only gateway-level existence is checked).
for (const p of messagingProviders) {
if (!providerExistsInGateway(p)) {
console.error(` ⚠ Messaging provider '${p}' was not found in the gateway.`);
console.error(` The credential may not be available inside the sandbox.`);
console.error(` To fix: openshell provider create --name ${p} --type generic --credential <KEY>`);
}
}

console.log(` ✓ Sandbox '${sandboxName}' created`);
return sandboxName;
}
Expand Down Expand Up @@ -3648,13 +3723,17 @@ async function onboard(opts = {}) {
}

module.exports = {
buildProviderArgs,
buildSandboxConfigSyncScript,
compactText,
copyBuildContextDir,
classifySandboxCreateFailure,
createSandbox,
formatEnvAssignment,
getFutureShellPathHint,
getGatewayStartEnv,
getGatewayReuseState,
getNavigationChoice,
getSandboxInferenceConfig,
getInstalledOpenshellVersion,
getRequestedModelHint,
Expand All @@ -3673,6 +3752,8 @@ module.exports = {
onboard,
onboardSession,
printSandboxCreateRecoveryHints,
providerExistsInGateway,
parsePolicyPresetEnv,
pruneStaleSandboxEntry,
repairRecordedSandbox,
recoverGatewayRuntime,
Expand All @@ -3684,6 +3765,9 @@ module.exports = {
isOpenclawReady,
arePolicyPresetsApplied,
setupPoliciesWithSelection,
summarizeCurlFailure,
summarizeProbeFailure,
upsertProvider,
hydrateCredentialEnv,
shouldIncludeBuildContextPath,
writeSandboxConfigSyncFile,
Expand Down
8 changes: 7 additions & 1 deletion test/credential-exposure.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ describe("credential exposure in process arguments", () => {
it("onboard.js does not embed sandbox secrets in the sandbox create command line", () => {
const src = fs.readFileSync(ONBOARD_JS, "utf-8");

expect(src).toMatch(/const sandboxEnv = \{ \.\.\.process\.env \};/);
// sandboxEnv must be built with a blocklist that strips all credential env vars
expect(src).toMatch(/blockedSandboxEnvNames/);
expect(src).toMatch(/NVIDIA_API_KEY/);
expect(src).toMatch(/BEDROCK_API_KEY/);
expect(src).toMatch(/DISCORD_BOT_TOKEN/);
expect(src).toMatch(/SLACK_BOT_TOKEN/);
expect(src).toMatch(/TELEGRAM_BOT_TOKEN/);
expect(src).toMatch(/streamSandboxCreate\(createCommand, sandboxEnv(?:, \{)?/);
expect(src).not.toMatch(/envArgs\.push\(formatEnvAssignment\("NVIDIA_API_KEY"/);
expect(src).not.toMatch(/envArgs\.push\(formatEnvAssignment\("DISCORD_BOT_TOKEN"/);
Expand Down
Loading