forked from NVIDIA/nemoclaw-community
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path02-providers.sh
More file actions
executable file
·378 lines (348 loc) · 17.3 KB
/
Copy path02-providers.sh
File metadata and controls
executable file
·378 lines (348 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Phase 3 of 4: Import v2 provider profiles and upsert this sandbox's providers.
# Outlook providers run an interactive Microsoft device-code login the first
# time (refresh token cached under .bootstrap/cache/, ignored by .gitignore).
# OUTLOOK_LOGIN_CACHE controls the cache: 0=off, 1=use (default), 2=force-rewrite.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$DIR/_lib.sh"
load_env
assert_messaging_config
# Confirm provider v2 is enabled (set once globally via `openshell settings set`).
if ! openshell settings get --global 2>/dev/null | grep -qE "providers_v2_enabled\s*=\s*true"; then
echo "providers_v2_enabled is not set at gateway-global scope." >&2
echo "Run: openshell settings set --global --key providers_v2_enabled --value true --yes" >&2
exit 1
fi
echo "Importing v2 provider profiles from $EXAMPLE_DIR/providers/"
# Delete-then-import so YAML edits land on re-run. `provider profile import`
# rejects existing IDs rather than upserting; ignoring delete errors covers
# first-run (nothing to delete) and any pre-existing custom profiles.
#
# A profile that is attached to a live sandbox CANNOT be deleted — the gateway
# returns FailedPrecondition ("in use by sandboxes: ..."). On a re-run (sandbox
# already exists) the delete is therefore a no-op and the import below would
# collide; the import loop tolerates that "already exists" case (the profile is
# already registered, just not re-importable while attached). To force a fresh
# import of an edited profile, delete the sandbox first (then its provider no
# longer holds the profile) and re-run.
for profile_id in nemoclaw-outlook-email nemoclaw-slack nemoclaw-github \
nemoclaw-atif-export-relay; do
openshell provider profile delete "$profile_id" >/dev/null 2>&1 || true
done
# Import each active profile by name. nemoclaw-compatible-endpoint is a
# forward-looking placeholder (see the header in providers/compatible-endpoint.yaml)
# and is deliberately NOT imported — the active inference path uses the
# built-in `nvidia` v2 profile via `openshell inference set` below.
#
# atif-export-relay.yaml carries __ATIF_RELAY_HOST/PORT__ placeholders so
# the endpoint tracks ATIF_RELAY_ENDPOINT — stage through sed before import.
STAGED_RELAY_PROFILE="$EXAMPLE_DIR/providers/.atif-export-relay.staged.yaml"
trap 'rm -f "$STAGED_RELAY_PROFILE"' EXIT
for profile_file in outlook-email.yaml slack.yaml github.yaml atif-export-relay.yaml; do
src="$EXAMPLE_DIR/providers/$profile_file"
if [[ "$profile_file" == "atif-export-relay.yaml" ]]; then
sed -e "s|__ATIF_RELAY_HOST__|$ATIF_RELAY_HOST|g" \
-e "s|__ATIF_RELAY_PORT__|$ATIF_RELAY_PORT|g" \
"$src" > "$STAGED_RELAY_PROFILE"
src="$STAGED_RELAY_PROFILE"
fi
# Tolerate the in-use case: if the delete above was refused because a live
# sandbox holds the profile, the profile is still registered and import
# reports "already exists". That's a no-op for our purposes (re-run), so
# don't abort the phase; surface anything else as a real failure.
if ! import_out="$(openshell provider profile import --file "$src" 2>&1)"; then
if grep -qi "already exists" <<<"$import_out"; then
echo " $profile_file: profile already registered (attached to a sandbox; not re-imported)"
else
printf '%s\n' "$import_out" >&2
exit 1
fi
fi
done
# Keep preflight subprocesses isolated from unrelated host state while
# preserving non-empty proxy and CA settings needed on enterprise networks.
# Do not define an unset OpenSSL override as an empty string: under `env -i`,
# SSL_CERT_FILE= or SSL_CERT_DIR= suppresses the platform's default trust-store
# discovery and makes every TLS endpoint appear untrusted.
PREFLIGHT_NETWORK_ENV=(
"HOME=$HOME"
"PATH=$PATH"
)
for preflight_env_name in \
HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
http_proxy https_proxy all_proxy no_proxy \
SSL_CERT_FILE SSL_CERT_DIR CURL_CA_BUNDLE REQUESTS_CA_BUNDLE; do
if [[ -n "${!preflight_env_name:-}" ]]; then
PREFLIGHT_NETWORK_ENV+=("$preflight_env_name=${!preflight_env_name}")
fi
done
unset preflight_env_name
# ── Inference provider (built-in nvidia v2 profile via inference.local) ─
INFERENCE_KEY="${OPENAI_API_KEY:-${COMPATIBLE_API_KEY:-}}"
INFERENCE_PREFLIGHT="${NEMOCLAW_INFERENCE_PREFLIGHT:-1}"
case "$INFERENCE_PREFLIGHT" in
0|1) ;;
*)
echo "Invalid NEMOCLAW_INFERENCE_PREFLIGHT=$INFERENCE_PREFLIGHT (expected 0 or 1)" >&2
exit 1
;;
esac
if [[ -n "$INFERENCE_KEY" ]]; then
INFERENCE_PROVIDER="compatible-endpoint"
INFERENCE_MODEL="${NEMOCLAW_MODEL:-nvidia/nemotron-3-super-120b-a12b}"
INFERENCE_BASE_URL="${NEMOCLAW_ENDPOINT_URL:-${OPENAI_BASE_URL:-https://integrate.api.nvidia.com/v1}}"
echo "Upserting inference provider $INFERENCE_PROVIDER (model: $INFERENCE_MODEL)"
# Recreate if existing provider has the wrong type (e.g. left over from the
# nemoclaw-compatible-endpoint direct-egress experiment).
if openshell provider get "$INFERENCE_PROVIDER" >/dev/null 2>&1 \
&& ! provider_type_matches "$INFERENCE_PROVIDER" nvidia; then
echo " $INFERENCE_PROVIDER exists with wrong type; recreating as nvidia"
openshell provider delete "$INFERENCE_PROVIDER" >/dev/null
fi
# Map our env var (OPENAI_API_KEY / COMPATIBLE_API_KEY) to the nvidia
# profile's expected NVIDIA_API_KEY at provider-create time.
if openshell provider get "$INFERENCE_PROVIDER" >/dev/null 2>&1; then
env -i HOME="$HOME" PATH="$PATH" NVIDIA_API_KEY="$INFERENCE_KEY" \
openshell provider update "$INFERENCE_PROVIDER" \
--credential NVIDIA_API_KEY --config "NVIDIA_BASE_URL=$INFERENCE_BASE_URL"
else
env -i HOME="$HOME" PATH="$PATH" NVIDIA_API_KEY="$INFERENCE_KEY" \
openshell provider create --name "$INFERENCE_PROVIDER" --type nvidia \
--credential NVIDIA_API_KEY --config "NVIDIA_BASE_URL=$INFERENCE_BASE_URL"
fi
if [[ "$INFERENCE_PREFLIGHT" == "1" ]]; then
echo "Validating inference endpoint, credential, model, and structured tool calls before sandbox creation"
env -i "${PREFLIGHT_NETWORK_ENV[@]}" \
NEMOCLAW_INFERENCE_PREFLIGHT_KEY="$INFERENCE_KEY" \
python3 "$DIR/inference_preflight.py" \
--endpoint "$INFERENCE_BASE_URL" \
--model "$INFERENCE_MODEL" \
--timeout "${NEMOCLAW_INFERENCE_PREFLIGHT_TIMEOUT_SECONDS:-10}"
else
echo "WARNING: inference preflight bypassed (NEMOCLAW_INFERENCE_PREFLIGHT=0)" >&2
fi
echo "Setting cluster inference: provider=$INFERENCE_PROVIDER model=$INFERENCE_MODEL"
openshell inference set --no-verify --provider "$INFERENCE_PROVIDER" --model "$INFERENCE_MODEL"
if [[ "$INFERENCE_PREFLIGHT" == "1" ]]; then
if ! active_route="$(openshell inference get 2>/dev/null)"; then
echo "Inference preflight failed (active-route): openshell inference get failed" >&2
exit 8
fi
env -i "${PREFLIGHT_NETWORK_ENV[@]}" \
python3 "$DIR/inference_preflight.py" \
--provider "$INFERENCE_PROVIDER" \
--model "$INFERENCE_MODEL" <<<"$active_route"
fi
else
if [[ "$INFERENCE_PREFLIGHT" == "1" ]]; then
echo "Inference preflight failed (configuration): neither OPENAI_API_KEY nor COMPATIBLE_API_KEY is set." >&2
echo "Set a credential, or set NEMOCLAW_INFERENCE_PREFLIGHT=0 for intentional offline setup." >&2
exit 1
fi
echo "WARNING: inference preflight bypassed and no credential is set. The agent will have no LLM." >&2
fi
# ── Outlook provider with gateway-managed OAuth refresh ─────────────────
if [[ -n "${OUTLOOK_CLIENT_ID:-}" ]]; then
OUTLOOK_PROVIDER="$SANDBOX_NAME-outlook"
OUTLOOK_LOGIN_CACHE_PATH="$EXAMPLE_DIR/.bootstrap/cache/ms-graph-token.json"
case "${OUTLOOK_LOGIN_CACHE:-1}" in
0|1|2) ;;
*) echo "Invalid OUTLOOK_LOGIN_CACHE=$OUTLOOK_LOGIN_CACHE (expected 0, 1, or 2)" >&2; exit 1 ;;
esac
login_json=""
mode="${OUTLOOK_LOGIN_CACHE:-1}"
reused_outlook_cache=false
cache_write_pending=false
outlook_device_login() {
local login_hint_args=()
[[ -n "${OUTLOOK_TARGET_MAILBOX:-}" ]] && login_hint_args+=(--login-hint "$OUTLOOK_TARGET_MAILBOX")
python3 "$DIR/login-ms-graph.py" \
--tenant-id "$OUTLOOK_TENANT_ID" \
--client-id "$OUTLOOK_CLIENT_ID" \
"${login_hint_args[@]}"
}
write_outlook_login_cache() {
local payload="$1" cache_dir cache_tmp
cache_dir="$(dirname "$OUTLOOK_LOGIN_CACHE_PATH")"
mkdir -p "$cache_dir"
umask 077
cache_tmp="$(mktemp "$cache_dir/.ms-graph-token.json.XXXXXX")"
if ! printf '%s\n' "$payload" > "$cache_tmp"; then
rm -f "$cache_tmp"
return 1
fi
if ! chmod 600 "$cache_tmp" || ! mv -f "$cache_tmp" "$OUTLOOK_LOGIN_CACHE_PATH"; then
rm -f "$cache_tmp"
return 1
fi
}
# Mode 1: try the cache, with a freshness check on the refresh-token
# horizon. expires_at_ms is the one-hour access-token expiry used by the
# gateway and must not decide whether device login is required.
if [[ "$mode" == "1" && -f "$OUTLOOK_LOGIN_CACHE_PATH" ]]; then
cached_expires_at_ms="$(
python3 "$DIR/lib/outlook_cache.py" "$OUTLOOK_LOGIN_CACHE_PATH" \
2>/dev/null || echo 0
)"
now_ms=$(( $(date +%s) * 1000 ))
if [[ "$cached_expires_at_ms" -gt "$now_ms" ]]; then
days_left=$(( (cached_expires_at_ms - now_ms) / 1000 / 86400 ))
echo "Reusing cached Microsoft refresh token at $OUTLOOK_LOGIN_CACHE_PATH (${days_left}d until expiry)"
login_json="$(cat "$OUTLOOK_LOGIN_CACHE_PATH")"
reused_outlook_cache=true
else
echo "Cached refresh token at $OUTLOOK_LOGIN_CACHE_PATH is expired or unreadable; re-running device-code login"
fi
fi
# Fall through to device-code login: mode 0, mode 2, or mode 1 cache miss/stale.
if [[ -z "$login_json" ]]; then
case "$mode" in
0) echo "OUTLOOK_LOGIN_CACHE=0 — device-code login, no on-disk cache" ;;
2) echo "OUTLOOK_LOGIN_CACHE=2 — forcing device-code login + cache rewrite" ;;
esac
login_json="$(outlook_device_login)"
# Modes 1 and 2 write only after the gateway successfully rotates the
# credential. This keeps a prior cache intact when configuration or
# rotation fails.
if [[ "$mode" != "0" ]]; then
cache_write_pending=true
fi
fi
echo "Upserting provider $OUTLOOK_PROVIDER (OAuth refresh-token strategy)"
if ! openshell provider get "$OUTLOOK_PROVIDER" >/dev/null 2>&1; then
openshell provider create --name "$OUTLOOK_PROVIDER" --type nemoclaw-outlook-email \
--credential "MS_GRAPH_ACCESS_TOKEN=bootstrap-placeholder"
fi
configure_outlook_refresh() {
local payload="$1" refresh_token expires_at_ms
refresh_token="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin)["refresh_token"])')"
expires_at_ms="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin)["expires_at_ms"])')"
openshell provider refresh configure "$OUTLOOK_PROVIDER" \
--credential-key MS_GRAPH_ACCESS_TOKEN \
--strategy oauth2-refresh-token \
--material "tenant_id=$OUTLOOK_TENANT_ID" \
--material "client_id=$OUTLOOK_CLIENT_ID" \
--material "refresh_token=$refresh_token" \
--secret-material-key refresh_token \
--credential-expires-at "$expires_at_ms"
}
outlook_refresh_status() {
NO_COLOR=1 openshell provider refresh status "$OUTLOOK_PROVIDER" \
--credential-key MS_GRAPH_ACCESS_TOKEN 2>&1 \
| sed $'s/\x1b\\[[0-9;]*m//g'
}
configure_outlook_refresh "$login_json"
rotate_output=""
if rotate_output="$(
openshell provider refresh rotate "$OUTLOOK_PROVIDER" \
--credential-key MS_GRAPH_ACCESS_TOKEN 2>&1
)"; then
[[ -z "$rotate_output" ]] || printf '%s\n' "$rotate_output"
if "$cache_write_pending"; then
write_outlook_login_cache "$login_json"
fi
else
[[ -z "$rotate_output" ]] || printf '%s\n' "$rotate_output" >&2
if ! "$reused_outlook_cache"; then
echo "Microsoft refresh-token rotation failed after device-code login; the login cache was not changed" >&2
exit 1
fi
refresh_status=""
if ! refresh_status="$(outlook_refresh_status)"; then
[[ -z "$refresh_status" ]] || printf '%s\n' "$refresh_status" >&2
echo "Could not classify the cached-token rotation failure; the existing login cache was preserved" >&2
exit 1
fi
if ! grep -qE 'token endpoint returned HTTP 400([^0-9]|$)' <<<"$refresh_status"; then
[[ -z "$refresh_status" ]] || printf '%s\n' "$refresh_status" >&2
echo "Cached-token rotation failed without a confirmed HTTP 400 endpoint rejection; the existing login cache was preserved" >&2
exit 1
fi
echo "The token endpoint rejected the cached Microsoft credential (HTTP 400); re-running device-code login"
fresh_login_json="$(outlook_device_login)"
configure_outlook_refresh "$fresh_login_json"
retry_output=""
if retry_output="$(
openshell provider refresh rotate "$OUTLOOK_PROVIDER" \
--credential-key MS_GRAPH_ACCESS_TOKEN 2>&1
)"; then
[[ -z "$retry_output" ]] || printf '%s\n' "$retry_output"
write_outlook_login_cache "$fresh_login_json"
else
[[ -z "$retry_output" ]] || printf '%s\n' "$retry_output" >&2
echo "Microsoft refresh-token rotation failed after device-code login; the existing login cache was preserved" >&2
exit 1
fi
fi
fi
# ── Slack provider (bot token + app token in one v2 provider) ──────────
if [[ -n "${SLACK_BOT_TOKEN:-}" || -n "${SLACK_APP_TOKEN:-}" ]]; then
SLACK_PROVIDER="$SANDBOX_NAME-slack"
echo "Validating Slack app token and Socket Mode scope before provider creation"
env -i "${PREFLIGHT_NETWORK_ENV[@]}" \
NEMOCLAW_SLACK_PREFLIGHT_TOKEN="${SLACK_APP_TOKEN:-}" \
NEMOCLAW_SLACK_PREFLIGHT_TIMEOUT_SECONDS="${NEMOCLAW_SLACK_PREFLIGHT_TIMEOUT_SECONDS:-10}" \
python3 "$DIR/slack_socket_preflight.py"
echo "Upserting provider $SLACK_PROVIDER (credentials: SLACK_BOT_TOKEN + SLACK_APP_TOKEN)"
upsert_cred "$SLACK_PROVIDER" nemoclaw-slack \
"SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN:-}" \
"SLACK_APP_TOKEN=${SLACK_APP_TOKEN:-}"
fi
# ── GitHub provider ─────────────────────────────────────────────────────
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
GH_PROVIDER="$SANDBOX_NAME-github"
echo "Upserting provider $GH_PROVIDER (credential: GITHUB_TOKEN)"
upsert_cred "$GH_PROVIDER" nemoclaw-github "GITHUB_TOKEN=$GITHUB_TOKEN"
fi
# ── ATIF object-storage provider (bearer token for atif-export-relay) ───
# Only configured when atif_remote_enabled returns true (i.e.,
# ATIF_EXPORT_MODE=relay). For local/unset, this block is skipped and ATIF
# writes go to the sandbox's /tmp/atif. The
# credential is a per-VM bearer token: the sandbox env carries a
# placeholder (`openshell:resolve:env:ATIF_RELAY_AUTH_TOKEN`), the L7
# proxy substitutes the real value on egress, and atif-export-relay
# validates it against the ATIF_RELAY_AUTH_TOKEN env var passed to the
# relay container (see extras/docker-compose.yml).
if atif_remote_enabled; then
# The bearer is a generated secret read from the gitignored cache (see
# _lib.sh atif_relay_token) — never from/into .env. 00-host-services.sh
# already brought the relay up with this same value, so there's no
# force-recreate here. Rotate by deleting the cache file (+ .registered).
ATIF_RELAY_AUTH_TOKEN="${ATIF_RELAY_AUTH_TOKEN:-$(atif_relay_token)}"
export ATIF_RELAY_AUTH_TOKEN
STORAGE_PROVIDER="$SANDBOX_NAME-atif-export-relay"
# Idempotency + rotation: every `provider update --credential` bumps the
# credential's internal revision, which invalidates a running sandbox's
# revisioned placeholder (set at sandbox-create time). So only (re)register
# when the token actually changed. `provider get` redacts values, so we
# record what we last registered as a fingerprint (sha256) beside the cache
# and compare — that detects rotation, which the redacted output cannot.
token_fp="$(printf '%s' "$ATIF_RELAY_AUTH_TOKEN" | sha256sum | cut -d' ' -f1)"
registered_fp=""
[[ -s "$ATIF_RELAY_TOKEN_CACHE.registered" ]] && registered_fp="$(cat "$ATIF_RELAY_TOKEN_CACHE.registered")"
needs_upsert=0
if ! openshell provider get "$STORAGE_PROVIDER" >/dev/null 2>&1; then
needs_upsert=1 # provider doesn't exist yet
elif [[ "$token_fp" != "$registered_fp" ]]; then
needs_upsert=1 # token rotated (or never recorded)
elif ! openshell provider get "$STORAGE_PROVIDER" 2>/dev/null \
| sed $'s/\x1b\\[[0-9;]*m//g' \
| grep -qE "^[[:space:]]*Credential keys:.*\\bATIF_RELAY_AUTH_TOKEN\\b"; then
needs_upsert=1 # safety net: credential key missing despite a matching fingerprint
fi
if [[ "$needs_upsert" == "1" ]]; then
echo "Upserting provider $STORAGE_PROVIDER (credential: ATIF_RELAY_AUTH_TOKEN)"
upsert_cred "$STORAGE_PROVIDER" nemoclaw-atif-export-relay \
"ATIF_RELAY_AUTH_TOKEN=$ATIF_RELAY_AUTH_TOKEN"
( umask 077; printf '%s\n' "$token_fp" > "$ATIF_RELAY_TOKEN_CACHE.registered" )
else
echo "Reusing existing $STORAGE_PROVIDER (token unchanged; skipping update to preserve sandbox placeholder revision)"
fi
fi
echo "Provider summary (this sandbox + shared inference):"
openshell provider list 2>&1 | grep -E "($SANDBOX_NAME|compatible-endpoint)" || true