Merge branch 'main' into codex/repo-audit-remediation-package-2
# Conflicts: # README.md # setup/scripts/install-packages
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
# ChatGPT Desktop.
|
||||
#
|
||||
# OpenAI ships macOS and Windows only. This is a community wrapper that converts
|
||||
# the upstream macOS disk image into a Linux Electron app and packages it as an
|
||||
# RPM, so the installed result is again something dnf owns.
|
||||
#
|
||||
# Same exception, same reason: there is no packaged form to prefer. Nothing is
|
||||
# pinned; `bootstrap-native` fetches the current upstream image each time and
|
||||
# fails loudly when it cannot.
|
||||
|
||||
description="ChatGPT Desktop, built into a Fedora RPM"
|
||||
repo="https://github.com/ilysenko/codex-desktop-linux.git"
|
||||
|
||||
# bootstrap-native installs build dependencies, builds, packages, and installs
|
||||
# the newest artifact -- so unlike the Claude build there is no separate install
|
||||
# step to do here.
|
||||
build() {
|
||||
make bootstrap-native
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Pinned signing keys
|
||||
|
||||
A key lands here when a publisher signs what Panama installs but does not
|
||||
publish the key, or its fingerprint, anywhere an install could fetch and check
|
||||
them first. Pinning the key is what lets `dnf` verify a download before root
|
||||
ever sees it.
|
||||
|
||||
Nothing here is a secret. These are public keys, and the reason to track them
|
||||
is that a *changed* one should be a merge request somebody reads, not a silent
|
||||
change of who is trusted.
|
||||
|
||||
## `RPM-GPG-KEY-chatgpt`
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Fingerprint | `3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4` |
|
||||
| User ID | `Codex Linux Repository` |
|
||||
| Signs | the `chatgpt` package and the repository metadata at `https://persistent.oaistatic.com/codex-app-prod/linux/rpm/$basearch` |
|
||||
| Used by | `setup/lib/chatgpt-package` |
|
||||
|
||||
Captured on 2026-08-27 from a machine where the official package had been
|
||||
installed, at `/etc/pki/rpm-gpg/RPM-GPG-KEY-chatgpt`, where the package's own
|
||||
root scriptlet writes it. It is the key that signed both the installed
|
||||
`chatgpt` package and the live `repodata/repomd.xml.asc`.
|
||||
|
||||
Be honest about what that is worth: OpenAI's documented instructions
|
||||
(<https://learn.chatgpt.com/docs/linux/linux-app>) are to download an RPM and
|
||||
install it, and they publish no key URL and no fingerprint to compare against.
|
||||
So this is trust established on first use and then held, not trust verified
|
||||
against the publisher. Held is the part that matters -- from here every machine
|
||||
checks the same fingerprint, and a swapped download fails instead of installing.
|
||||
|
||||
To re-derive the fingerprint from the file:
|
||||
|
||||
```bash
|
||||
gpg --show-keys --with-colons setup/keys/RPM-GPG-KEY-chatgpt \
|
||||
| awk -F: '$1 == "fpr" { print $10; exit }'
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGpypFUBEACi1Vvzq9pIpA6lj7chbqELuxJtVuzUzxrasa6ZU0yF4yhq7jf8
|
||||
3YkJRHwbezBKeQyzJ5lkX0EhXS8aXxUhMAm3PFpAlwcInfKzmV7atJwvaxIw6Rmd
|
||||
GYe9fBWKjTN/SmPIjtyxrTznZY97+TfD1AeGZpLaJ8fsnhrC+HkiN2TACiTocgpe
|
||||
hFiP0OWK7mWZeTWnY2scpIYXP1Ro7nQv4KacmY4JacTQ7m/HM0Qej/3olhuEv2Cw
|
||||
lMVWw57/oHhmTllfLDQOogFQyIVqaaR98y/Eu6cAabSfcsqAAZ2A8vfHYD27z28J
|
||||
vLO2PZEJd5ThlnX4Zqv0eIpZdBj//8Sl/MSqTshFZ1NDsRoqwdqw284X5MpnOJ4k
|
||||
4Sc2Se8tJxt/nCeibH3dJ504Fb1X/mnOqhCAQ6pVJz4RB5HRlFPSkxVPyag1v1m/
|
||||
7T4vie+OR4eqFQNz6mudrOoMmeVIfyL5fbe4cOr4fk/FyvEE2xMgkFatPqXn7vM9
|
||||
og+zremPCfwRAFpBPyX74VowFY7llcdaj/w8K5T8PzM14Hb3E4ZKizMluKmTvTq9
|
||||
WE1/eSQJLLQqXD5VmtmdUaC/VyE/1ZlIxcA1LWqvEQ327UXREvX/nHsrkKrl956W
|
||||
jzkiHFUTsD1NJ0dMfs+csOt8Furb5jZj+HsMmCm9jLdfz5b/4WKLPbvxIwARAQAB
|
||||
tBZDb2RleCBMaW51eCBSZXBvc2l0b3J5iQJRBBMBCgA7FiEEO/oOSui4zBai2bpo
|
||||
SjtKVmxGYOQFAmpypFUCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ
|
||||
SjtKVmxGYORlCQ/9FyikZo8HQcJBP9E/oXVPds/fQnIFB2qJR2z3DrfYEonNt/ev
|
||||
SAySkPPq4/mEOjaI0pFlDDGSaps+FTcJFgoVRTasBIF7JJivvjW9ap8iWEbhhVLe
|
||||
IrFLbMLpUcTRntUx7R4fVMJ/1/cGn+NWZmNwS9ORorzSyCH0IAgCw1Xc3ZrjuMbF
|
||||
VjdToMC1TiXXCEmlYpQakmQ3Ay1cH0FHC2BBNn1MNVkJdPhpZIZCdhaMPHfYFpyo
|
||||
pg8wFvZ5iIcvlbMgyuy8CPJVRWUcYy2dOhEOGnYJnXRPkE3E1hf8YOHNzRlduH89
|
||||
6lT9qcEK2+fpLfrVGoc4zscLZ+Ey+Ko6iQRdVE1j67+wNR3hX8ukue574v1N/xxu
|
||||
i575jumSE19lEj1sH4+P4gFHOtTbF0JhKKzLctbga0IAwTPKhnt3qzj1U5Yj/MZS
|
||||
uEVjrLhdRauOuFBXUclgyVf2w/lE85UUOdlcollsYA6Huq7xDamqf8SslZQGre3E
|
||||
I+lhpqJR1cOwDMUzzcl40uTyhrxXXd/bk4QSlhZbwHR25Pnt+ZMtWavlQWS0eDEV
|
||||
8djuXAURCmx5WOqAFB/TJe1mn5EvyWg4VFzrY/NVNOpzgY5+Xp7J28z7f637r712
|
||||
Eu9j4imVcdPigwS+jf/0f81i2o9b82Y26TN8+EtDLCY841MJ1lrjDrX/dno=
|
||||
=Y+3h
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,97 @@
|
||||
# Installing OpenAI's ChatGPT Desktop without trusting the download. Sourced,
|
||||
# not run.
|
||||
#
|
||||
# OpenAI signs both its packages and its repository metadata, with one key, and
|
||||
# publishes neither that key nor its fingerprint anywhere a first install could
|
||||
# fetch them. The documented instructions are "download this RPM and install
|
||||
# it" -- and the RPM's own root scriptlet is what writes the repository file and
|
||||
# drops the key into /etc/pki/rpm-gpg. Following them means handing an
|
||||
# unverified download to root and letting it decide afterwards what to trust,
|
||||
# which is the one thing this repository will not do with a network response.
|
||||
#
|
||||
# So the key is pinned here instead. setup/keys/ carries a copy and records
|
||||
# where it came from; this verifies that copy's fingerprint, installs it, and
|
||||
# writes the repository itself with gpgcheck on. dnf then checks the metadata
|
||||
# signature and the package signature against that key before anything runs as
|
||||
# root, and every later upgrade goes through the same repository and the same
|
||||
# key.
|
||||
#
|
||||
# Two callers, which is why this is a library: install-packages, for a machine
|
||||
# being built, and the migration that replaces the community codex-desktop
|
||||
# build on machines that predate the official package.
|
||||
|
||||
# The key that signs the packages and the repository metadata. Pinned, so a
|
||||
# substituted key is a failure here rather than a silent change of publisher.
|
||||
CHATGPT_KEY_FINGERPRINT="3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4"
|
||||
|
||||
# `$basearch` stays literal: dnf expands it, and this is the same base URL the
|
||||
# package's own scriptlet configures.
|
||||
CHATGPT_REPO_BASEURL="https://persistent.oaistatic.com/codex-app-prod/linux/rpm/\$basearch"
|
||||
CHATGPT_REPO_FILE="/etc/yum.repos.d/chatgpt.repo"
|
||||
CHATGPT_KEY_FILE="/etc/pki/rpm-gpg/RPM-GPG-KEY-chatgpt"
|
||||
|
||||
chatgpt_pinned_key() {
|
||||
printf '%s/setup/keys/RPM-GPG-KEY-chatgpt' "${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
}
|
||||
|
||||
# The fingerprint of the pinned copy. Nonzero when it cannot be read at all,
|
||||
# which the caller reports differently from a key that reads but is the wrong
|
||||
# one.
|
||||
chatgpt_pinned_fingerprint() {
|
||||
local key
|
||||
key="$(chatgpt_pinned_key)"
|
||||
[[ -r "$key" ]] || return 1
|
||||
gpg --show-keys --with-colons "$key" 2>/dev/null \
|
||||
| awk -F: '$1 == "fpr" { print $10; exit }'
|
||||
}
|
||||
|
||||
# Fails without touching anything when the pinned key is missing, unreadable,
|
||||
# or not the key this repository says it is. Everything below assumes it passed.
|
||||
chatgpt_verify_pinned_key() {
|
||||
local found
|
||||
if ! command -v gpg >/dev/null 2>&1; then
|
||||
printf 'gpg is missing, so the pinned ChatGPT signing key cannot be verified.\n' >&2
|
||||
return 1
|
||||
fi
|
||||
if ! found="$(chatgpt_pinned_fingerprint)"; then
|
||||
printf 'The pinned ChatGPT signing key is missing: %s\n' "$(chatgpt_pinned_key)" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$found" != "$CHATGPT_KEY_FINGERPRINT" ]]; then
|
||||
printf 'The pinned ChatGPT signing key is %s, not the expected %s.\n' \
|
||||
"${found:-unreadable}" "$CHATGPT_KEY_FINGERPRINT" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Installs the verified key and the repository that names it, so the install
|
||||
# after this one is a signature check rather than an act of faith.
|
||||
#
|
||||
# Takes the command that gets root, because the two callers ask for it
|
||||
# differently: plain `sudo` from the installer, which authenticated once at the
|
||||
# top of the run, and `panama-sudo --reason ...` from a migration, whose prompt
|
||||
# has to say which repair it is for.
|
||||
chatgpt_install_repository() {
|
||||
local -a sudo_cmd=("$@")
|
||||
(( ${#sudo_cmd[@]} > 0 )) || sudo_cmd=(sudo)
|
||||
|
||||
chatgpt_verify_pinned_key || return 1
|
||||
|
||||
"${sudo_cmd[@]}" install -D -m 0644 "$(chatgpt_pinned_key)" "$CHATGPT_KEY_FILE" || return 1
|
||||
"${sudo_cmd[@]}" rpmkeys --import "$CHATGPT_KEY_FILE" || return 1
|
||||
|
||||
# Written here rather than left to the package's scriptlet, because the
|
||||
# point of it is to exist -- with gpgcheck on and this key named -- before
|
||||
# the first install rather than after it. Same base URL and same key the
|
||||
# scriptlet writes, so it finds nothing to change later.
|
||||
printf '%s\n' \
|
||||
'[openai-chatgpt]' \
|
||||
'name=ChatGPT' \
|
||||
"baseurl=$CHATGPT_REPO_BASEURL" \
|
||||
'enabled=1' \
|
||||
'type=rpm-md' \
|
||||
'gpgcheck=1' \
|
||||
'repo_gpgcheck=1' \
|
||||
"gpgkey=file://$CHATGPT_KEY_FILE" \
|
||||
| "${sudo_cmd[@]}" tee "$CHATGPT_REPO_FILE" >/dev/null || return 1
|
||||
}
|
||||
@@ -89,6 +89,11 @@ source "$PANAMA_PATH/setup/lib/extras-catalog"
|
||||
source "$PANAMA_PATH/setup/lib/machine-role"
|
||||
ROLE="$(panama_role)"
|
||||
|
||||
# Establishing the verified ChatGPT repository, shared with the migration that
|
||||
# replaces the community build, so neither can install it a less careful way.
|
||||
# shellcheck source=../lib/chatgpt-package
|
||||
source "$PANAMA_PATH/setup/lib/chatgpt-package"
|
||||
|
||||
# One list, installed the way every list is installed: --skip-unavailable so a
|
||||
# single rotted name cannot cost the transaction, then report_missing so a
|
||||
# skipped name is a warning somebody reads.
|
||||
@@ -1299,6 +1304,28 @@ if ! install_claude_desktop_if_trusted; then
|
||||
softly_failed+=("Claude Desktop")
|
||||
fi
|
||||
|
||||
# ChatGPT Desktop: OpenAI ships an official Linux RPM now. Panama used to build
|
||||
# a community wrapper from the macOS disk image -- it was `panama app
|
||||
# chatgpt-desktop` -- because no packaged form existed; that build froze often
|
||||
# and carried its own local rebuild daemon. The official package is strictly
|
||||
# better: it comes from a repository, so it upgrades with every other package
|
||||
# from then on.
|
||||
#
|
||||
# The repository and its signing key are established first, from the copy
|
||||
# pinned in setup/keys/, so dnf verifies the metadata and the package before
|
||||
# either reaches root. Upstream's own instructions do not allow that -- see
|
||||
# setup/lib/chatgpt-package for why they are not followed here.
|
||||
if rpm -q chatgpt >/dev/null 2>&1; then
|
||||
log "ChatGPT Desktop already installed"
|
||||
elif ! chatgpt_install_repository sudo; then
|
||||
log "Could not establish the verified ChatGPT repository; skipping"
|
||||
softly_failed+=("ChatGPT Desktop")
|
||||
else
|
||||
log "Installing ChatGPT Desktop..."
|
||||
sudo dnf install -y chatgpt > /dev/null \
|
||||
|| { log "ChatGPT Desktop install failed; skipping"; softly_failed+=("ChatGPT Desktop"); }
|
||||
fi
|
||||
|
||||
# The RPM ships rustdesk.service already enabled, which is what provides
|
||||
# unattended access; Panama deliberately does not start it a second time.
|
||||
install_rustdesk || true
|
||||
|
||||
+25
-23
@@ -11,12 +11,11 @@
|
||||
# this repository, not anybody's personal content, so a stranger who clones
|
||||
# Panama wants it for exactly the same reason its author does.
|
||||
#
|
||||
# ~/.claude/skills was a single symlink into user/agents/skills until now, and
|
||||
# a directory cannot be two things at once. So the destination becomes a real
|
||||
# directory and every skill -- shipped here, personal from user/ -- is linked
|
||||
# into it one at a time. link-user runs after this stage on purpose: it links
|
||||
# last, so a personal skill named like a shipped one wins, which is the
|
||||
# precedence Claude Code itself uses.
|
||||
# ~/.agents/skills and ~/.claude/skills may each start as a single symlink into
|
||||
# user/agents/skills, but a directory cannot point at personal and shipped
|
||||
# skills at once. Both destinations become real directories with one link per
|
||||
# skill. link-user runs after this stage on purpose, so a personal skill named
|
||||
# like a shipped one wins in every agent runtime.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -25,7 +24,7 @@ log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
SKILLS_DIR="$PANAMA_PATH/skills"
|
||||
PANAMA_OLD="$PANAMA_PATH/config/old"
|
||||
DESTINATION="$HOME/.claude/skills"
|
||||
DESTINATIONS=("$HOME/.agents/skills" "$HOME/.claude/skills")
|
||||
|
||||
[[ -d "$SKILLS_DIR" ]] || { log "No skills/ in this checkout; nothing to link."; exit 0; }
|
||||
|
||||
@@ -51,27 +50,30 @@ displace() {
|
||||
log "Moved existing $destination to $backup"
|
||||
}
|
||||
|
||||
# The destination itself has to be a real directory before anything can be
|
||||
# linked into it. An old whole-directory symlink is removed; a regular file
|
||||
# somebody left at this path is kept, in config/old/.
|
||||
mkdir -p "$(dirname "$DESTINATION")"
|
||||
if [[ -L "$DESTINATION" ]]; then
|
||||
rm -f "$DESTINATION"
|
||||
log "Removed the old $DESTINATION symlink; skills are linked one by one now"
|
||||
elif [[ -e "$DESTINATION" && ! -d "$DESTINATION" ]]; then
|
||||
displace "$DESTINATION"
|
||||
fi
|
||||
mkdir -p "$DESTINATION"
|
||||
# Each destination has to be a real directory before anything can be linked
|
||||
# into it. An old whole-directory symlink is removed; a regular file somebody
|
||||
# left at the path is kept in config/old/.
|
||||
for destination in "${DESTINATIONS[@]}"; do
|
||||
mkdir -p "$(dirname "$destination")"
|
||||
if [[ -L "$destination" ]]; then
|
||||
rm -f "$destination"
|
||||
log "Removed the old $destination symlink; skills are linked one by one now"
|
||||
elif [[ -e "$destination" && ! -d "$destination" ]]; then
|
||||
displace "$destination"
|
||||
fi
|
||||
mkdir -p "$destination"
|
||||
done
|
||||
|
||||
linked=0
|
||||
for skill in "$SKILLS_DIR"/*; do
|
||||
[[ -e "$skill" ]] || continue
|
||||
name="$(basename "$skill")"
|
||||
target="$DESTINATION/$name"
|
||||
|
||||
displace "$target"
|
||||
ln -s "$skill" "$target"
|
||||
log "Linked skills/$name → $target"
|
||||
for destination in "${DESTINATIONS[@]}"; do
|
||||
target="$destination/$name"
|
||||
displace "$target"
|
||||
ln -s "$skill" "$target"
|
||||
log "Linked skills/$name → $target"
|
||||
done
|
||||
linked=$(( linked + 1 ))
|
||||
done
|
||||
|
||||
|
||||
Reference in New Issue
Block a user