Codex: connect to DevGrail (MCP + login)
Sets up Codex inside this workspace so it can drive DevGrail itself — containers, projects, scripts, files, and terminal sessions — scoped to the DevGrail API key supplied at run time. Registers the `devgrail` streamable HTTP MCP server in `${CODEX_HOME:-~/.codex}/config.toml` using the container's `DEVGRAIL_MCP_URL`. Via the included **Copy of Codex: sign in without a browser (auth)** script it also installs a Codex login — pasted, or a secret backed by `/home/dev/.codex/auth.json` in a workspace that is already signed in. The combined form asks for both sets of parameters. An existing registration is left untouched unless **Replace existing registration** is `yes`. Configuration is backed up before changes. **Security:** the DevGrail API key is stored in cleartext as an Authorization header in Codex config. Prefer a key limited to what this workspace needs. After it runs: start `codex` and ask it to list your DevGrail containers.
What it asks for
- DevGrail API keyrequired
- The key Codex will send to the DevGrail MCP server. Its grants decide what the agent can see and do; prefer a narrowly scoped key.
- MCP URL (override)
- Leave empty on current workspace images; the URL comes from DEVGRAIL_MCP_URL. For older images, enter the full DevGrail MCP URL.
- Replace existing registration
- Choose 'yes' to replace an existing devgrail MCP entry after rotating the API key or changing the URL. With 'no', an existing entry is left untouched.
What it runs
The whole script, exactly as it will execute in your workspace. Nothing is hidden — read it before you run it.
#!/usr/bin/env sh
set -eu
umask 077
log() { printf '[devgrail-mcp] %s\n' "$*"; }
die() { printf '[devgrail-mcp] ERROR: %s\n' "$*" >&2; exit 1; }
MCP_NAME="devgrail"
API_KEY="${DEVGRAIL_API_KEY:-}"
MCP_URL="${DEVGRAIL_MCP_URL_OVERRIDE:-${DEVGRAIL_MCP_URL:-}}"
REPLACE="${REPLACE_EXISTING:-no}"
CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
CONFIG_FILE="$CODEX_DIR/config.toml"
BACKUP_FILE="$CONFIG_FILE.bak"
HAD_CONFIG="no"
EXISTING="no"
command -v codex >/dev/null 2>&1 \
|| die "the 'codex' CLI is not on PATH — run this in a workspace that has Codex installed"
[ -n "$API_KEY" ] || die "no DevGrail API key was supplied"
[ -n "$MCP_URL" ] || die "DEVGRAIL_MCP_URL is not set in this container — fill in the 'MCP URL (override)' parameter"
# Static TOML strings require a single-line token. DevGrail API keys use this
# conservative URL-safe character set.
case "$API_KEY" in
*[!A-Za-z0-9._~-]*) die "the DevGrail API key contains characters that cannot be stored safely in config.toml" ;;
esac
mkdir -p "$CODEX_DIR"
if codex mcp get "$MCP_NAME" >/dev/null 2>&1; then
EXISTING="yes"
if [ "$REPLACE" != "yes" ]; then
log "MCP server '$MCP_NAME' is already registered — nothing to do."
log "To point it at a new key or URL, re-run with 'Replace existing registration' = yes."
exit 0
fi
fi
if [ -f "$CONFIG_FILE" ]; then
HAD_CONFIG="yes"
cp -p "$CONFIG_FILE" "$BACKUP_FILE"
log "backed up existing Codex config to $BACKUP_FILE"
fi
restore_config() {
if [ "$HAD_CONFIG" = "yes" ]; then
cp -p "$BACKUP_FILE" "$CONFIG_FILE"
else
rm -f "$CONFIG_FILE"
fi
}
if [ "$EXISTING" = "yes" ]; then
codex mcp remove "$MCP_NAME" >/dev/null 2>&1 \
|| { restore_config; die "could not remove the existing '$MCP_NAME' entry"; }
log "removed the existing '$MCP_NAME' entry so it can be re-added"
fi
codex mcp add "$MCP_NAME" --url "$MCP_URL" </dev/null >/dev/null \
|| { restore_config; die "codex mcp add failed"; }
HEADER_LINE="http_headers = { Authorization = \"Bearer $API_KEY\" }"
tmp="$CONFIG_FILE.devgrail.tmp"
trap 'rm -f "$tmp"' 0
if awk -v header="$HEADER_LINE" '
BEGIN { in_target = 0; inserted = 0 }
$0 == "[mcp_servers.devgrail]" {
in_target = 1
print
next
}
/^\[/ {
if (in_target && !inserted) {
print header
inserted = 1
}
in_target = 0
}
{
if (in_target && $0 ~ /^[[:space:]]*http_headers[[:space:]]*=/) {
if (!inserted) {
print header
inserted = 1
}
next
}
print
}
END {
if (in_target && !inserted) {
print header
inserted = 1
}
if (!inserted) exit 42
}
' "$CONFIG_FILE" > "$tmp"; then
mv "$tmp" "$CONFIG_FILE"
trap - 0
else
restore_config
die "could not add the authorization header to $CONFIG_FILE"
fi
chmod 600 "$CONFIG_FILE"
codex mcp get "$MCP_NAME" >/dev/null 2>&1 \
|| { restore_config; die "'$MCP_NAME' was added but cannot be read back — restored the previous config"; }
log "registered MCP server '$MCP_NAME' -> $MCP_URL"
log "Security note: the bearer token is stored in cleartext in $CONFIG_FILE."
log "Next: run 'codex' and ask it to list your DevGrail containers."
Included scripts
These come along automatically when you add this script.
Codex: sign in without a browser (auth)
#!/usr/bin/env sh
set -eu
umask 077
log() { printf '[codex-setup] %s\n' "$*"; }
warn() { printf '[codex-setup] WARNING: %s\n' "$*" >&2; }
die() { printf '[codex-setup] ERROR: %s\n' "$*" >&2; exit 1; }
AUTH_JSON="${CODEX_AUTH_JSON:-}"
OVERWRITE="${OVERWRITE_EXISTING_AUTH:-yes}"
CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
AUTH_FILE="$CODEX_DIR/auth.json"
CONFIG_FILE="$CODEX_DIR/config.toml"
[ -n "$AUTH_JSON" ] || die "no Codex auth JSON was supplied"
command -v codex >/dev/null 2>&1 \
|| die "the 'codex' CLI is not on PATH — run this in a workspace that has Codex installed"
if [ "$(id -u)" -eq 0 ]; then SUDO=""
elif command -v sudo >/dev/null 2>&1; then SUDO="sudo -n"
else SUDO=""
fi
# jq is optional: it validates the copied auth file. Never fail merely because
# jq cannot be installed; the login copy is still useful.
# This script body is piped to sh on stdin, so every installer gets </dev/null.
if ! command -v jq >/dev/null 2>&1; then
log "jq not found; attempting install (optional)"
export DEBIAN_FRONTEND=noninteractive
if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -qq </dev/null && $SUDO apt-get install -y -qq jq </dev/null || true
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y jq </dev/null || true
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y jq </dev/null || true
elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache jq </dev/null || true
elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -Sy --noconfirm jq </dev/null || true
elif command -v zypper >/dev/null 2>&1; then $SUDO zypper install -y jq </dev/null || true
elif command -v brew >/dev/null 2>&1; then brew install jq </dev/null || true
fi
command -v jq >/dev/null 2>&1 || warn "continuing without jq"
fi
mkdir -p "$CODEX_DIR"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$AUTH_JSON" | jq empty >/dev/null 2>&1 \
|| die "the supplied value is not valid JSON — paste the whole auth.json contents, from '{' to '}'"
fi
if [ -f "$AUTH_FILE" ] && [ "$OVERWRITE" != "yes" ]; then
log "$AUTH_FILE already exists and overwrite is off — leaving it alone"
else
if [ -f "$AUTH_FILE" ]; then
cp -p "$AUTH_FILE" "$AUTH_FILE.bak"
log "backed up previous credentials to $AUTH_FILE.bak"
fi
printf '%s\n' "$AUTH_JSON" > "$AUTH_FILE"
chmod 600 "$AUTH_FILE"
log "wrote $AUTH_FILE (mode 600)"
fi
# A copied auth.json must be selected explicitly when a credential store is
# available. Keep all unrelated Codex configuration intact.
if [ -f "$CONFIG_FILE" ]; then
tmp="$CONFIG_FILE.devgrail.tmp"
trap 'rm -f "$tmp"' 0
if grep -Eq '^[[:space:]]*cli_auth_credentials_store[[:space:]]*=' "$CONFIG_FILE"; then
sed 's/^[[:space:]]*cli_auth_credentials_store[[:space:]]*=.*$/cli_auth_credentials_store = "file"/' "$CONFIG_FILE" > "$tmp"
else
{
printf '%s\n\n' 'cli_auth_credentials_store = "file"'
cat "$CONFIG_FILE"
} > "$tmp"
fi
if cmp -s "$CONFIG_FILE" "$tmp"; then
rm -f "$tmp"
trap - 0
log "$CONFIG_FILE already selects file-based credential storage"
else
cp -p "$CONFIG_FILE" "$CONFIG_FILE.bak"
mv "$tmp" "$CONFIG_FILE"
trap - 0
log "updated $CONFIG_FILE (backup: $CONFIG_FILE.bak)"
fi
else
printf '%s\n' 'cli_auth_credentials_store = "file"' > "$CONFIG_FILE"
log "created $CONFIG_FILE"
fi
chmod 600 "$CONFIG_FILE"
log "done. Run 'codex' — it should start without asking you to log in."
How to get it
Open your DevGrail workspace, go to Scripts → Official scripts, and choose Download. It is copied into your own library, where you can edit it, run it, or set it as a workspace default — the copy is yours, and later changes here do not touch it.