ChatGPT-App „SkillPilot Coach v1“: Deployment und Cutover
Stand: 2. August 2026
Status: Der mehrsprachige MCP-Coach ist der aktuelle ChatGPT-Entwicklungs- und
Produktkandidat; der interne Arbeitsstand 1.0.0-SNAPSHOT zielt auf die noch
nicht öffentlich veröffentlichte Paketversion 1.0.0.
Die Clientbindung wird nach vollständiger Prüfung des ausgewählten
OAuth-Clientprofils und erneutem Workflow-Acceptance-Test allgemein
freigegeben. Der V1-Vertrag verwendet normales HTTPS und OAuth/PKCE auf dem
dedizierten mcp-coach-v1.skillpilot.com-Origin. Client-TLS ist nicht
aktiviert.
Dieses Runbook aktiviert den mehrsprachigen, chat-first MCP-Lerncoach mit einer eng
begrenzten read-only MCP-UI für Lernzielvisualisierungen. MCP-Server,
OAuth-Authorization-Server, UI-Ressourcenauslieferung und SkillPilot-Fachlogik
laufen im bestehenden Spring-Boot-Prozess. Der Node-MCP-Server unter
ai/openai app/ bleibt ein lokales Regressionstestbett; dort liegt zugleich die
geprüfte Quellimplementierung der Lernzielkarte, deren selbstenthaltenes
Build-Artefakt in den Spring-Boot-Ressourcenpfad übernommen wird.
Die Architektur- und Migrationsentscheidungen stehen in
openai-mcp-coach-migration-plan.md.
Der verbindliche Identitäts- und Sitzungsablauf steht getrennt in
openai-mcp-oauth-learner-session-architecture.md.
Paket-, Contract- und Lifecycle-Versionen folgen dem
Versionierungs- und Lebenszyklusplan;
Release, Rollback und Stilllegung folgen dem
V1-Release-Runbook.
Insbesondere verwaltet ChatGPT OAuth Access- und Refresh-Token automatisch;
Benutzer geben niemals OAuth-Token, OAuth-Client-Secret oder dauerhafte
SkillPilot-ID im Chat ein. Jeder ausdrückliche Start in SkillPilot erzeugt
jedoch eine davon unabhängige, absolut 24 Stunden gültige
learningSessionId. SkillPilot trägt diese Referenz automatisch in die
Startnachricht ein; ChatGPT übergibt sie unverändert an jedes fachliche
MCP-Werkzeug.
1. Öffentlicher Vertrag
| Zweck | URL |
|---|---|
| Plugin-Identität | skillpilot-coach-v1 |
| MCP Server URL | https://mcp-coach-v1.skillpilot.com/mcp |
| OAuth Resource / Audience | https://mcp-coach-v1.skillpilot.com/mcp |
| Widget-Origin | https://mcp-coach-v1.skillpilot.com |
| Lernzielbild-Ressource | ui://skillpilot/coach/v1/sha256-c890cf271307d815256450a2b20b27d57015a84e9f4e39c97532eaefc4e30c26/goal-visualization.html |
| Protected Resource Metadata | https://mcp-coach-v1.skillpilot.com/.well-known/oauth-protected-resource/mcp |
| Domain-Challenge | https://mcp-coach-v1.skillpilot.com/.well-known/openai-apps-challenge |
| OAuth Issuer | https://skillpilot.com/api/openai/v1 |
| Authorization-Server-Metadata | https://skillpilot.com/.well-known/oauth-authorization-server/api/openai/v1 |
| Authorization Endpoint | https://skillpilot.com/api/openai/v1/oauth2/authorize |
| Token Endpoint | https://skillpilot.com/api/openai/v1/oauth2/token |
| Revocation Endpoint | https://skillpilot.com/api/openai/v1/oauth2/revoke |
Die Tabelle nennt die Ressource, die der aktuelle Tool-Descriptor aktiv
referenziert. Bereits in Browser- oder nativen App-Chats gespeicherte
Nachrichten dürfen jedoch weiterhin die frühere, ebenfalls
inhaltsadressierte Ressource
ui://skillpilot/coach/v1/sha256-157aab83e83d6fcf208c4a1ae138c020aa4f117e9b990ba78d029b570fb9644c/goal-visualization.html
anfordern. Der V1-Server hält deshalb beide URIs über resources/list und
resources/read bytegenau verfügbar; nur die Ressource mit sha256-c890...
ist das aktive Template für neue Tool-Ergebnisse.
Der additive V1-vHost reicht ausschließlich den öffentlichen Pfad /mcp an
den loopback-gebundenen Spring-Transport /internal/openai/v1/mcp weiter.
Es gibt keinen öffentlichen Kompatibilitätsalias. Der produktive App-Eintrag
verwendet ausschließlich diese V1-Server URL, nicht den Entwicklungstunnel
und nicht einen Pfad auf skillpilot.com.
2. Discovery-Bootstrap und OAuth-Werte
Der Produktivvertrag verwendet Authorization Code mit PKCE S256 und genau
einen vorregistrierten vertraulichen OAuth-Client für die Linie
SkillPilot Coach v1. Dessen feste Client-ID, langes zufälliges
Client-Secret, exakte Callback-Allowlist, feste MCP-Resource und feste Scopes
werden vom App-Autor auf beiden Seiten konfiguriert. ChatGPT authentisiert sich
am Token-Endpunkt mit client_secret_basic; SkillPilot akzeptiert weder
none noch offene Dynamic Client Registration, CIMD oder
private_key_jwt im aktiven Produktivprofil.
Das Secret ist ausschließlich geschützte Konfiguration in ChatGPT und SkillPilot. Es gehört weder in Repository, Browser, Startnachricht, Toolargumente noch Logs. PKCE bindet zusätzlich den Authorization Code an den von ChatGPT erzeugten Verifier. Normales serverauthentisiertes HTTPS bleibt Pflicht. Eine Clientzertifikat-Infrastruktur ist nicht Teil des V1-Vertrags.
Die ChatGPT-Verwaltung prüft die MCP-URL, bevor sie ihre erweiterten OAuth- Einstellungen zeigt. Gleichzeitig benötigt der vollständige SkillPilot- Authorization-Server die app-spezifische Callback-URL. Dafür existiert ein expliziter, datenloser Bootstrapmodus:
- Vollbetrieb deaktiviert lassen und ausschließlich
SKILLPILOT_OPENAI_COACH_V1_BOOTSTRAP_ENABLED=truesetzen. - Nach dem Restart die vier Discovery-URLs und den konstanten MCP-
401verifizieren. Der Bootstrap registriert weder Tools noch OAuth-Client, Token-Endpunkte, Lernerdienste oder einen Coach-Health-Contributor. - In der ChatGPT-App-Verwaltung
Server URL, die produktive MCP-URL undOAuthwählen. - Eine feste, nur dieser App zugeordnete Client-ID und ein langes zufälliges Client-Secret erzeugen. Dieselben Werte in ChatGPT und SkillPilot konfigurieren; das Secret nie in Dokumentation oder Tickets kopieren.
- Die dort angezeigte app-spezifische Produktions-Callback-URL der Form
https://chatgpt.com/connector/oauth/{callback_id}unverändert übernehmen. - Mehrere echte Callback-URLs als kommaseparierte Liste konfigurieren. Keine Beispiel- oder Legacy-URL ergänzen, die nicht in der App-Verwaltung steht.
- In ChatGPT als Token-Endpunkt-Authentisierung
client_secret_basicauswählen. Bootstrap ausschalten und Vollbetrieb mit Client-ID, Secret, Callback, OAuth, MCP und aktivierten Schreiboperationen atomar aktivieren. Ein read-only Canary ist ein bewusst nicht produktionsbereiter Diagnosezustand.
Der Bootstrap-MCP-Endpunkt weist jede Methode und auch beliebige Bearer-
Werte mit 401 plus WWW-Authenticate ab. Authorization-, Token-, Revocation-
und Introspection-Endpunkte bleiben dabei 404. Die OpenAI-Dokumentation
definiert diesen Challenge-/Metadata-Vertrag als Discovery-Mechanismus; ob ein
konkreter ChatGPT-UI-Build damit seine erweiterten Einstellungen freischaltet,
wird dennoch praktisch geprüft. Bei einem UI-Fehler wird der Sicherheitsvertrag
nicht gelockert. Das lokale Rate-Limit und die datensparsame Status-Telemetrie
schützen bereits diesen öffentlichen Bootstrap-Rand; bei mehreren Instanzen
bleibt zusätzlich ein gemeinsames Gateway-Limit erforderlich.
Ohne Client-ID oder Callback-Liste bricht der Spring-Start bei aktiviertem OpenAI-V1-OAuth absichtlich ab. Bootstrap und Vollbetrieb dürfen ebenfalls nicht gleichzeitig aktiviert sein; diese Fehlkonfiguration bricht den Start ab.
3. Runtime-Konfiguration
Für den sicheren Cutover können Code und additive Liquibase-Migration zunächst
in einem getrennten read-only Canary geprüft werden. Der produktive
Vollbetrieb benötigt dagegen aktivierte Schreiboperationen und
verwendet normales serverauthentisiertes HTTPS am dedizierten V1-vHost und
verpflichtendes OAuth/PKCE mit exakter Resource-/Audience- und Scope-Prüfung.
Der separate Host isoliert Domainverifikation und Plugin-Lifecycle; er aktiviert
kein mTLS. Der bestehende skillpilot.com-vHost wird nicht grundsätzlich
umgebaut; er erhält nur die unten beschriebene enge 404-Sperre gegen
MCP-/Internpfad-Aliasse.
Die reproduzierbare additive vHost-Konfiguration liegt unter
deploy/nginx/skillpilot-mcp-coaches.conf. Sie MUSS innerhalb des vorhandenen
Nginx-http {}-Blocks eingebunden werden. Eine Einbindung auf globaler Ebene
vor http {} führt zu server directive is not allowed here und ist
unzulässig. Die zugehörige Certbot-Lineage
skillpilot-mcp-coaches umfasst nach der Umstellung exakt die bereits
angelegten Major-Hosts V1 bis V9:
mcp-coach-v1.skillpilot.com
mcp-coach-v2.skillpilot.com
mcp-coach-v3.skillpilot.com
mcp-coach-v4.skillpilot.com
mcp-coach-v5.skillpilot.com
mcp-coach-v6.skillpilot.com
mcp-coach-v7.skillpilot.com
mcp-coach-v8.skillpilot.com
mcp-coach-v9.skillpilot.com
Die zuvor lokal vorbereiteten Namen mcp-coach-de-v* und
mcp-coach-en-v* gehörten zu keiner Veröffentlichung. Sie werden weder in
Nginx weitergeleitet noch als Kompatibilitätsroute erhalten. Verbliebene DNS-
Einträge oder alte Zertifikat-SANs dürfen nach dem V1-Cutover entfernt werden.
Nur V1 wird an Spring weitergeleitet. Die übrigen acht HTTPS-vHosts sind
reserviert und liefern für jeden Pfad 404; ihre DNS- und TLS-Bereitschaft ist
keine Veröffentlichung. Änderungen werden immer zuerst mit nginx -t geprüft
und erst danach per Reload aktiviert. Bestehende vHosts werden weder ersetzt
noch grundsätzlich umgebaut.
Zusätzlich wird
deploy/nginx/skillpilot-main-vhost-openai-deny-locations.conf ausschließlich
innerhalb des bestehenden HTTPS-server {}-Blocks für skillpilot.com
und dort vor dessen allgemeinem location / eingebunden. Dieses enge
Location-Snippet sperrt den verworfenen öffentlichen Pfad, den internen
Spring-Transport und das interne Protected-Resource-Metadata-Ziel am
Haupt-Origin mit 404. Es darf weder auf globaler Ebene
noch im http {}-Block eingebunden werden. So bleibt der bestehende Haupt-vHost
ansonsten unverändert, und nur der dedizierte V1-vHost veröffentlicht den
MCP-Vertrag.
Vor einer Installation werden die bestehende Nginx-Hauptdatei und bereits vorhandene Ziel-Snippets unter eindeutigen Namen gesichert. Eine vorhandene, funktionierende MCP-vHost-Datei wird zuerst gegen die Repository-Vorlage verglichen und nicht blind überschrieben:
Zuvor muss der Produktions-Checkout nachweislich bereits den sprachneutralen Cutover enthalten. Diese Prüfung ist verpflichtend: Ein altes Skript würde weiter den DE-Host testen; ein alter vHost kann unbekannte neue Hostnamen als Default-vHost irrtümlich an V1 weiterleiten. Alle fünf Befehle müssen erfolgreich sein und der letzte darf keine Ausgabe erzeugen:
grep -F 'server_name mcp-coach-v1.skillpilot.com;' \
deploy/nginx/skillpilot-mcp-coaches.conf
grep -F 'mcp-coach-v9.skillpilot.com' \
deploy/nginx/skillpilot-mcp-coaches.conf
grep -F 'proxy_pass http://127.0.0.1:8787/internal/openai/v1/mcp;' \
deploy/nginx/skillpilot-mcp-coaches.conf
grep -F 'MCP_ORIGIN="https://mcp-coach-v1.skillpilot.com"' \
scripts/verify_openai_v1_public_edge.sh
! grep -E 'mcp-coach-(de|en)-v[1-9]' \
deploy/nginx/skillpilot-mcp-coaches.conf
Schlägt eine Prüfung fehl, wird nichts nach /etc/nginx installiert. Zuerst
muss der freigegebene Commit gepullt werden. Ein erfolgreicher Lauf eines
veralteten Smoke-Skripts ist kein Ersatz für diesen Inhaltscheck.
sudo cp -a -n /etc/nginx/nginx.conf \
/etc/nginx/nginx.conf.before-mcp-subdomain-includes
if sudo test -e /etc/nginx/skillpilot-mcp-coaches.conf; then
sudo cp -a -n /etc/nginx/skillpilot-mcp-coaches.conf \
/etc/nginx/skillpilot-mcp-coaches.conf.before-repository-sync
sudo diff -u /etc/nginx/skillpilot-mcp-coaches.conf \
deploy/nginx/skillpilot-mcp-coaches.conf || true
fi
if sudo test -e /etc/nginx/skillpilot-main-vhost-openai-deny-locations.conf; then
sudo cp -a -n \
/etc/nginx/skillpilot-main-vhost-openai-deny-locations.conf \
/etc/nginx/skillpilot-main-vhost-openai-deny-locations.conf.before-repository-sync
sudo diff -u \
/etc/nginx/skillpilot-main-vhost-openai-deny-locations.conf \
deploy/nginx/skillpilot-main-vhost-openai-deny-locations.conf || true
fi
Erst nach Prüfung des Diffs werden aus dem Repository-Root die beiden Dateien getrennt installiert:
sudo install -o root -g root -m 0644 \
deploy/nginx/skillpilot-mcp-coaches.conf \
/etc/nginx/skillpilot-mcp-coaches.conf
sudo install -o root -g root -m 0644 \
deploy/nginx/skillpilot-main-vhost-openai-deny-locations.conf \
/etc/nginx/skillpilot-main-vhost-openai-deny-locations.conf
Die zwei Includes haben absichtlich verschiedene Kontexte:
http {
# bestehende globale Einstellungen bleiben unverändert
include /etc/nginx/skillpilot-mcp-coaches.conf;
server {
server_name skillpilot.com skillpilot.org skillpilot.mobi;
# vor dem bestehenden allgemeinen `location /`
include /etc/nginx/skillpilot-main-vhost-openai-deny-locations.conf;
location / {
# bestehende SkillPilot-Proxykonfiguration
}
}
}
Der Ausschnitt ist eine Platzierungshilfe und kein Ersatz für den bestehenden
Haupt-vHost. Das zweite Include wird gezielt in genau diesen vorhandenen
server {}-Block aufgenommen; weitere Einträge bleiben unverändert. Danach
wird immer zuerst die vollständige Konfiguration geprüft. Nur ein erfolgreicher
Test erlaubt den Reload:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl is-active nginx
Vor dem öffentlichen V1-Smoke muss die bestehende Let's-Encrypt-Lineage auf
die neun sprachneutralen Major-Hosts umgestellt werden. Zuerst müssen alle
vorhandenen DNS-A-/AAAA-Einträge auf denselben Server zeigen; ein fehlender
AAAA-Eintrag ist zulässig, ein veralteter AAAA-Eintrag dagegen nicht. Nach den
Backups und der
sichtbaren Diff-Prüfung wird der additive Repository-vHost installiert und mit
nginx -t sowie Reload aktiviert, damit der Port-80-vHost alle HTTP-Challenges
bedienen kann. Der bestehende Haupt-vHost wird dabei nicht ersetzt. Erst danach
folgen Dry-Run und tatsächliche Erneuerung derselben Lineage:
sudo certbot certonly --nginx --dry-run \
--preferred-challenges http \
--key-type ecdsa \
--cert-name skillpilot-mcp-coaches \
-d mcp-coach-v1.skillpilot.com \
-d mcp-coach-v2.skillpilot.com \
-d mcp-coach-v3.skillpilot.com \
-d mcp-coach-v4.skillpilot.com \
-d mcp-coach-v5.skillpilot.com \
-d mcp-coach-v6.skillpilot.com \
-d mcp-coach-v7.skillpilot.com \
-d mcp-coach-v8.skillpilot.com \
-d mcp-coach-v9.skillpilot.com
sudo certbot certonly --nginx \
--preferred-challenges http \
--key-type ecdsa \
--cert-name skillpilot-mcp-coaches \
--force-renewal \
-d mcp-coach-v1.skillpilot.com \
-d mcp-coach-v2.skillpilot.com \
-d mcp-coach-v3.skillpilot.com \
-d mcp-coach-v4.skillpilot.com \
-d mcp-coach-v5.skillpilot.com \
-d mcp-coach-v6.skillpilot.com \
-d mcp-coach-v7.skillpilot.com \
-d mcp-coach-v8.skillpilot.com \
-d mcp-coach-v9.skillpilot.com
sudo certbot certificates
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl is-active nginx
Der zweite Aufruf ersetzt den Domain-Satz derselben Lineage; er erzeugt keine
parallele Sprachlinie. Vor einer Bestätigung muss die von Certbot angezeigte
Domainliste exakt auf V1 bis V9 geprüft werden. Danach muss V1 seinen
geschützten Vertrag liefern und V2 bis V9 müssen über gültiges TLS für jeden
Pfad 404 liefern; ./scripts/verify_openai_v1_public_edge.sh prüft beides.
--force-renewal ist hier bewusst gesetzt, weil der erfolgreich geprüfte neue
SAN-Satz sofort in dieselbe Lineage geschrieben werden soll. --expand ist
ungeeignet: Es würde die alten, nicht veröffentlichten DE-/EN-SANs beibehalten,
statt den Domain-Satz exakt zu ersetzen.
SERVER_ADDRESS=127.0.0.1
SKILLPILOT_PUBLIC_BASE_URL=https://skillpilot.com
# Unabhängig vom OAuth-Client-Secret erzeugen, z. B.: openssl rand -hex 32
SKILLPILOT_SIGNING_SECRET=<mindestens-32-hochentropische-zeichen>
SKILLPILOT_OPENAI_COACH_V1_ENABLED=true
SKILLPILOT_OPENAI_COACH_V1_BOOTSTRAP_ENABLED=false
SKILLPILOT_OPENAI_COACH_V1_OAUTH_ENABLED=true
SKILLPILOT_OPENAI_COACH_V1_MCP_ENABLED=true
SKILLPILOT_OPENAI_COACH_V1_WRITES_ENABLED=true
SKILLPILOT_OPENAI_CHATGPT_URL=https://chatgpt.com/
SKILLPILOT_OPENAI_COACH_V1_OAUTH_CLIENT_AUTHENTICATION_METHOD=client_secret_basic
SKILLPILOT_OPENAI_COACH_V1_OAUTH_CLIENT_ID=<exakte-feste-client-id-dieser-app>
SKILLPILOT_OPENAI_COACH_V1_OAUTH_CLIENT_SECRET=<langes-zufälliges-client-secret>
SKILLPILOT_OPENAI_COACH_V1_OAUTH_REDIRECT_URIS=<exakte-callback-url-oder-kommaliste>
# Nur bei einem tatsächlichen Client-ID-Wechsel, einmalig und danach entfernen:
# SKILLPILOT_OPENAI_COACH_V1_OAUTH_LEGACY_CLIENT_IDS=<exakte-alte-client-id-oder-kommaliste>
SKILLPILOT_OPENAI_SECURE_COOKIE=true
SKILLPILOT_OPENAI_LEARNING_SESSION_TTL=PT24H
SKILLPILOT_OPENAI_CLEANUP_INTERVAL_MS=3600000
SKILLPILOT_OPENAI_OAUTH_ACCESS_TOKEN_TTL=PT1H
SKILLPILOT_OPENAI_OAUTH_REFRESH_TOKEN_TTL=P30D
SKILLPILOT_OPENAI_RATE_LIMIT_ENABLED=true
SKILLPILOT_OPENAI_RATE_LIMIT_WINDOW=PT1M
SKILLPILOT_OPENAI_RATE_LIMIT_MCP_REQUESTS=120
SKILLPILOT_OPENAI_RATE_LIMIT_OAUTH_REQUESTS=60
SKILLPILOT_OPENAI_RATE_LIMIT_UI_REQUESTS=60
SKILLPILOT_OPENAI_RATE_LIMIT_METADATA_REQUESTS=120
SKILLPILOT_OPENAI_RATE_LIMIT_MAX_CLIENT_BUCKETS=10000
Die drei öffentlichen V1-URLs werden nicht als Umgebungsvariablen konfiguriert. Sie sind unveränderliche Bestandteile des V1-Vertrags:
- MCP und OAuth-Resource:
https://mcp-coach-v1.skillpilot.com/mcp - Protected-Resource-Metadaten:
https://mcp-coach-v1.skillpilot.com/.well-known/oauth-protected-resource/mcp
Auch ein SKILLPILOT_OPENAI_COACH_V1_UI_ORIGIN ist unzulässig: Der Widget-Origin ist
als https://mcp-coach-v1.skillpilot.com fest im V1-Vertrag verankert und
wird identisch über _meta.ui.domain und _meta["openai/widgetDomain"]
ausgeliefert. Alte URL-Variablen und gleichnamige neue Override-Versuche führen
fail-closed zum Abbruch. Damit kann eine alte oder falsch geschriebene Route den
versionierten V1-Vertrag nicht unbemerkt ersetzen.
Vor dem ersten Subdomain-Deployment werden insbesondere alte Einträge für
SKILLPILOT_OPENAI_DE_UI_ORIGIN, SKILLPILOT_OPENAI_DE_V1_ORIGIN,
SKILLPILOT_OPENAI_DE_MTLS_EDGE_ENABLED,
SKILLPILOT_OPENAI_DE_MTLS_EDGE_TRUSTED_PROXIES und mTLS-Smoke-Zertifikate aus
der EnvironmentFile entfernt. Sie gehören nicht zum 1.0.0-Vertrag. Ebenso
müssen SKILLPILOT_OPENAI_DE_MCP_URL,
SKILLPILOT_OPENAI_DE_OAUTH_RESOURCE und
SKILLPILOT_OPENAI_DE_RESOURCE_METADATA vollständig entfernt werden.
./deploy_skillpilot.sh prüft vor Asset-Kopien, Build und Service-Restart die
tatsächlich von der systemd-Unit referenzierte EnvironmentFile. Der
Produktionsvertrag erlaubt genau eine solche Datei, standardmäßig
/etc/skillpilot/skillpilot.env; für einen abweichenden Pfad muss
SKILLPILOT_SERVICE_ENV_FILE ausdrücklich gesetzt werden. Aus der Datei werden
ausschließlich Namen entfernter OpenAI-V1-Variablen erkannt; ihre Werte und
alle OAuth-, Datenbank- oder anderen Secrets werden weder protokolliert noch
ausgegeben. Dieselben alten Namen dürfen auch nicht über Environment=,
PassEnvironment= oder die globale systemd-Umgebung eingeschleust werden.
Ist die eine EnvironmentFile in systemd optional (ignore_errors=yes) und
fehlt, akzeptiert der Preflight nach Prüfung der übrigen Umgebungskanäle die
kanonischen V1-Defaults. Eine fehlende verpflichtende Datei
(ignore_errors=no) bleibt ein Fehler.
Eine EnvironmentFile mit OAuth- oder Datenbank-Secrets bleibt root:root und
0600. Ihre Rechte dürfen für den Deployment-Preflight nicht gelockert werden.
Kann der Deploy-Benutzer die root-geschützte Datei oder einen Elternordner nicht
lesen beziehungsweise durchlaufen, meldet der Preflight diesen Inhaltscheck
sichtbar als SKIP; die exakte Spring-Startprüfung bleibt die finale
fail-closed-Grenze. Eine allgemeine sudo cat-Freigabe oder weltlesbare
Secret-Datei ist ausdrücklich nicht zulässig.
Die Migration ist absichtlich fail-closed: alte SKILLPILOT_OPENAI_DE_*-
und SKILLPILOT_OPENAI_COACH_DE_V1_*-Namen werden nicht als stille
V1-Aliasse übernommen. Alle V1-spezifischen
Werte tragen SKILLPILOT_OPENAI_COACH_V1_*; gemeinsame Richtlinien des
einzigen Spring-Prozesses tragen SKILLPILOT_OPENAI_* ohne Sprach- oder
Versionssegment. Alte Namen werden entfernt, nicht leer gesetzt.
Die neun Nginx-Origins sind keine neun Spring-Prozesse und werden nicht über
eine gemeinsame URL-Umgebungsvariable umgeschaltet. Jeder öffentliche Host
wird in Nginx fest auf den internen Pfad seiner Vertragslinie abgebildet. Nur
V1 ist derzeit implementiert; die übrigen reservierten Hosts antworten
absichtlich mit 404. Für spätere Linien gilt bereits jetzt diese eindeutige
Namenskonvention:
| öffentlicher Host | Spring-Konfigurationsgruppe | linienbezogene Environment-Namen | aktueller Status |
|---|---|---|---|
mcp-coach-v1.skillpilot.com |
skillpilot.openai.coach.v1 |
SKILLPILOT_OPENAI_COACH_V1_* |
aktiv, intern /internal/openai/v1/* |
mcp-coach-v2.skillpilot.com |
skillpilot.openai.coach.v2 |
SKILLPILOT_OPENAI_COACH_V2_* |
reserviert, 404 |
mcp-coach-v3.skillpilot.com |
skillpilot.openai.coach.v3 |
SKILLPILOT_OPENAI_COACH_V3_* |
reserviert, 404 |
mcp-coach-v4.skillpilot.com |
skillpilot.openai.coach.v4 |
SKILLPILOT_OPENAI_COACH_V4_* |
reserviert, 404 |
mcp-coach-v5.skillpilot.com |
skillpilot.openai.coach.v5 |
SKILLPILOT_OPENAI_COACH_V5_* |
reserviert, 404 |
mcp-coach-v6.skillpilot.com |
skillpilot.openai.coach.v6 |
SKILLPILOT_OPENAI_COACH_V6_* |
reserviert, 404 |
mcp-coach-v7.skillpilot.com |
skillpilot.openai.coach.v7 |
SKILLPILOT_OPENAI_COACH_V7_* |
reserviert, 404 |
mcp-coach-v8.skillpilot.com |
skillpilot.openai.coach.v8 |
SKILLPILOT_OPENAI_COACH_V8_* |
reserviert, 404 |
mcp-coach-v9.skillpilot.com |
skillpilot.openai.coach.v9 |
SKILLPILOT_OPENAI_COACH_V9_* |
reserviert, 404 |
Die reservierten Namen sind eine Konvention, noch keine akzeptierte
Laufzeitkonfiguration. Der aktuelle Server bricht beim Setzen einer noch nicht
implementierten Linie oder eines unbekannten linienbezogenen Namens ab, statt
den Eintrag still zu ignorieren. Eine Linie wird erst mit eigenem Vertrag,
interner Route, Spring-Konfigurationsgruppe und Tests aktiviert. Gemeinsame
Prozesswerte wie Cookie-Härtung, Session-TTLs und Rate Limits bleiben einmalig
unter SKILLPILOT_OPENAI_*.
Auch SKILLPILOT_SERVER_BUILD wird nicht in
/etc/skillpilot/skillpilot.env gepflegt. Gradle baut genau ein Artefakt
skillpilot-server und bettet den vollständigen
lowercase Commit von HEAD beim Verarbeiten der Backend-Ressourcen in
skillpilot.openai.coach.v1.server-build und
skillpilot.openai.coach.v1.mcp.server-version ein. scripts/deploy.sh prüft beide
Werte gegen den tatsächlich ausgecheckten Commit, bevor der Dienst neu
gestartet wird. Die Telemetrie und Health-Ausgabe beschreiben dadurch das
ausgelieferte Artefakt und keinen manuell nachgetragenen Umgebungswert.
SKILLPILOT_OPENAI_COACH_V1_WRITES_ENABLED=true ist für den funktionsfähigen
Produktivcoach verpflichtend. Personalisierung, Navigation, Aufgabenfortschritt
und Mastery sind fachlich schreibende Vorgänge. Bei false funktionieren
Discovery, OAuth und lesende Werkzeuge weiterhin, aber der Coach bricht beim
ersten erforderlichen Zustandswechsel mit 503 Service Unavailable ab. Dieser
Wert ist deshalb ausschließlich für einen bewusst isolierten read-only Canary
geeignet. Die Betriebsabschaltung darf keine erneute OAuth-Verbindung auslösen.
Der normale aktivierte Provider startet ausschließlich im sicheren
Clientmodus; es gibt keinen produktiven secure-mode=false-Schalter. Der
sichere Clientmodus verlangt client_secret_basic und prüft die exakte
Client-ID, das Secret in konstantzeitgeeigneter Form, Redirect-Allowlist,
Resource, Scopes und PKCE S256. Fehlendes Secret, none,
private_key_jwt, DCR, CIMD und jeder stille Profil-Fallback brechen den Start
beziehungsweise den Tokenaustausch fail-closed ab.
Auch SKILLPILOT_SIGNING_SECRET ist für den aktivierten OpenAI-V1-Provider
verpflichtend. Der Prozess bricht den Start ab, wenn der Wert fehlt, dem
unsicheren Platzhalter entspricht, weniger als 32 Nicht-Leerzeichen enthält
oder strukturell zu wenig Entropie aufweist. Der Wert wird in dieser Prüfung
weder protokolliert noch in einer Fehlermeldung wiedergegeben. Er ist ein
eigenständiges HMAC-Betriebsgeheimnis und darf nicht mit dem OAuth-Client-
Secret identisch sein.
Das Client-Secret wird als Betriebsgeheimnis verwaltet: restriktive Dateirechte, keine Shell-History, kein Request-/Health-Detail, keine Clientausgabe. Bei einer Rotation werden zunächst ChatGPT und SkillPilot koordiniert umgestellt, anschließend alle mit dem alten Clientvertrag ausgestellten Tokens widerrufen.
Die Legacy-Client-Allowlist ist nicht Bestandteil des Basisprofils. Sie wird
nur bei einem tatsächlichen Client-ID-Wechsel verwendet. SkillPilot entfernt
dann ausschließlich für die exakt genannten Altclients deren OAuth-
Authorizations, Consents, registrierte Clientzeilen sowie Access- und Refresh-
Tokens. Historische Verbindungs-, Binding-Grant- und Pending-Launch-
Altartefakte dürfen beim einmaligen Cutover zusätzlich bereinigt werden. Die
heutigen openai_de_learning_session-Datensätze werden durch eine reine
OAuth-Clientrotation dagegen nicht gelöscht. Anschließend muss nur der neu
konfigurierte Client lesbar sein; andernfalls bricht der Start ab.
Vor diesem Cutover ist ein Datenbank-Backup Pflicht. Die Einstellung ist idempotent für bereits entfernte IDs, soll aber nach dem erfolgreichen Produktionsstart wieder aus dem Environment entfernt werden. Alte Access- und Refresh-Tokens sowie Autorisierungen sind absichtlich unwiderruflich ungültig; ein reines Anwendungs-Rollback stellt sie nicht wieder her. Benutzer autorisieren die SkillPilot-App danach einmal neu.
Die aktuelle additive Migration stellt
openai_de_learning_session als eigenständige Startberechtigung bereit. Der
Datensatz enthält mindestens den Hash der zufälligen learningSessionId, die
interne Lernendenreferenz, started_at und expires_at. Die dauerhafte
SkillPilot-ID und der Klartext der Session-ID werden dort nicht dupliziert.
Alte, ausschließlich über ein OAuth-Subject adressierte Sitzungszeilen sind
kein Fallback und werden beim Cutover kontrolliert entfernt oder migriert.
Das historische physische Tabellenpräfix openai_de_ bleibt bei dieser
additiven Umstellung bewusst als interner Migrationsanker bestehen. Es ist kein
Sprach- oder Vertragsmerkmal mehr; die verbindliche Kommunikationssprache
steht pro Sitzung in communication_locale. Ein Umbenennen bestehender
Tabellen und interner Java-Typen wäre eine eigenständige, risikoreichere
Datenbankmigration ohne Nutzen für den öffentlichen V1-Vertrag und gehört
nicht in diesen Cutover.
Der Start-Intent ist ein kurzlebiger Auftrag an das Fachbackend. Bei jedem
Klick auf Lernen starten wendet SkillPilot den eng typisierten Intent unter
Learner-Lock auf den autoritativen Zustand an und erzeugt unmittelbar danach
eine neue kryptografisch zufällige learningSessionId. Auch zwei Starts
desselben Lernenden erzeugen verschiedene IDs. Die absolute Frist wird durch
SKILLPILOT_OPENAI_LEARNING_SESSION_TTL gesteuert und beträgt produktiv
PT24H. MCP-Aufrufe, Access-Token-Refresh, Reload und neue oder parallele
Chats verlängern sie nicht.
Die Session-ID wird einmal automatisch in den URL-codierten Startprompt
eingetragen und danach von ChatGPT unverändert als Pflichtargument an jedes
lernendenbezogene Tool weitergegeben. SkillPilot löst ausschließlich den Hash
auf den Lernenden auf. OAuth allein erzeugt oder wählt keine Lernsession; eine
Session-ID allein autorisiert keinen MCP-Aufruf. Fehlt einer der beiden
Nachweise, liefert der Fachvertrag SESSION_REQUIRED beziehungsweise einen
OAuth-Fehler. Ein Fallback vom OAuth-Subject auf einen Lernenden ist
unzulässig.
Das Cockpit startet ausschließlich über
POST /api/ui/learners/{skillpilotId}/openai/v1/launch. Ein erfolgreicher
Aufruf wendet den typisierten Start-Intent an und erzeugt genau eine neue
Lernsession samt Startprompt. Jeder weitere Aufruf erzeugt unabhängig von
Browser, bestehender App-Autorisierung oder früheren Starts eine neue Session.
Der /launch-Aufruf ist eigenständig und benötigt keinen vorgeschalteten
Verbindungsstatus oder kurzlebigen Browser-Zwischenzustand.
Abgelaufene oder widerrufene Lernsession-Datensätze werden unabhängig vom
OAuth-Lebenszyklus abgewiesen und bereinigt. Authorization Codes, Access- und
Refresh-Tokens sowie Consents folgen ausschließlich ihrem eigenen OAuth-
Lebenszyklus. Insbesondere erzeugt Tokenausgabe oder Token-Refresh keine
Lernsession und verlängert keine bestehende. Das Intervall der technischen
Bereinigung steuert SKILLPILOT_OPENAI_CLEANUP_INTERVAL_MS; der
Standardwert ist eine Stunde.
Jeder /launch-Aufruf muss providerEligibilityConfirmed=true ausdrücklich
mitsenden. Fehlt die Bestätigung oder ist sie falsch, weist das Backend den
Start mit 403 ab. Das Cockpit fragt sie einmal pro Browser-Sitzung ab: Die
lernende Person bestätigt damit, mindestens 13 Jahre alt zu sein, jede am
Aufenthaltsort geltende höhere Altersgrenze zu erfüllen und unter 18 die
Erlaubnis eines Elternteils oder einer erziehungsberechtigten Person zu haben.
SkillPilot leitet das Alter nicht aus Klassenstufe oder Curriculum ab und
speichert dafür weder Geburtsdatum noch Altersprofil. Es handelt sich um eine
bewusste Selbstbestätigung, nicht um eine Identitäts- oder Altersverifikation
und nicht um OAuth-Client- oder Lernendenidentität.
Der Coach-Pfad benötigt keinen OpenAI-Modell-API-Key. Modell und Chat werden vom ChatGPT-Konto der lernenden Person bereitgestellt; SkillPilot stellt nur MCP-, OAuth- und Fachbackend bereit.
Die konfigurierte MCP-Resource ist sicherheitsrelevant und wird absichtlich
exakt behandelt. Weder führende oder nachgestellte Leerzeichen noch eine
zusätzliche abschließende / werden akzeptiert. Der Authorization Request
wird mit diesem Wert persistiert; Token-Introspektion prüft ihn bei jedem
MCP-Aufruf erneut als Audience. Derselbe Schutz gilt nach Refresh-Token-
Rotation.
3.1 Health, Readiness und Metriknamen
Bei SKILLPILOT_OPENAI_COACH_V1_ENABLED=true registriert Spring den Health-Contributor
openAiDeCoach. Er fließt in die Actuator-Gruppe readiness ein. Der Beitrag
ist nur UP, wenn MCP und OAuth aktiviert sind, die erforderlichen Client- und
Callback-Werte gesetzt sind, die öffentlichen MCP-/Metadata-Ziele gültiges
HTTPS verwenden und der erwartete Vertrag mit genau zwölf Werkzeugen geladen ist.
Die Readiness-Gruppe enthält zusätzlich den Datenbank-Health-Check db; ein
nicht erreichbarer Persistenzdienst darf daher nicht als einsatzbereiter Coach
gemeldet werden.
SKILLPILOT_OPENAI_COACH_V1_WRITES_ENABLED=false ist ein erlaubter read-only
Canary-Zustand und setzt die gemeinsame Readiness nicht auf DOWN. Ob der
vollständige Coach produktiv funktionsfähig ist, muss deshalb zusätzlich über
die Betriebsumgebung beziehungsweise einen separaten Deployment-Preflight
geprüft werden.
Die Health-Details enthalten ausschließlich nicht geheime Statuswerte, darunter
serverBuild, serverBuildConfigured, mcpEnabled, oauthEnabled,
writesEnabled, secureMode,
clientAuthenticationMethod, publicClientConfigured,
privateKeyJwtConfigured, clientIdConfigured, redirectUrisConfigured,
contractToolCount, contractHash, rateLimitEnabled und
rateLimitConfigured. Der
serverBuild ist der im Backend-Artefakt eingebettete Git-Commit;
contractHash ist ein deterministischer SHA-256-Hash über Serverinstruktionen
und öffentliche Tooldeskriptoren. Client-ID, Callback-URLs, MCP-URL, Tokens,
SkillPilot-IDs und Lerninhalte werden nicht ausgegeben. Health-Details dürfen
nur über den internen, geschützten Managementzugang freigegeben werden.
Die exakten Micrometer-Namen heißen:
skillpilot.openai.coach.v1.mcp.tool.duration
skillpilot.openai.coach.v1.operational.event
Der Timer besitzt aus dem Anwendungscode ausschließlich die begrenzten Tags tool
(zwölf bekannte Toolnamen oder unknown) und status (success, error oder
exception). Der Timer liefert Aufrufzahl und Dauer. Argumente, Prompts,
Antworten, Lernenden- oder Verbindungskennungen und OAuth-Werte sind weder Tags
noch Messdaten. Ein konfigurierter Exporter kann zusätzliche globale
Infrastruktur-Tags ergänzen; auch diese dürfen keine personenbezogenen Werte
enthalten.
Der zweite Name ist ein Counter mit genau einem begrenzten Tag event. Er
erfasst ausschließlich oauth_failure, refresh_failure, session_required,
http_401, http_403, http_409, http_429, timeout, replay_rejected,
cross_provider_rejected und tool_exception. Es gibt keine dynamischen
Fehlertexte, Kennungen, Pfade oder Lerninhalte als Tags. Cross-Learner-/IDOR-
Abwehr wird zusätzlich in negativen Integrationstests geprüft; der MCP-Vertrag
nimmt absichtlich keine Lernendenkennung als Toolargument entgegen.
Der lokale Limiter trennt MCP, OAuth, Cockpit-Start und Metadaten und verwendet
nur die vom Servlet-Container normalisierte Netzwerkadresse. Er parst keine
Forwarding-Header. Deshalb muss der produktive Reverse Proxy eingehende
Forwarded-/X-Forwarded-*-Header verwerfen beziehungsweise selbst ersetzen,
und der Backendport darf nicht direkt aus dem Internet erreichbar sein. Bei
mehreren Backendinstanzen ist zusätzlich ein gemeinsames Gateway-Limit
verbindlich; der In-Process-Limiter ist bewusst nur eine letzte lokale
Schutzschicht. Eine Ablehnung antwortet mit 429, Retry-After und no-store.
Der allgemeine Request-Body-Logger überspringt MCP-, Provider-OAuth- und OpenAI-Cockpit-Startpfade vollständig. Insbesondere Authorization Codes, PKCE-Verifier, Access-/Refresh-Tokens und typisierte Lernziel-Startintents dürfen auch bei aktiviertem Debug-Logging nicht als Request-Body in den Anwendungslogs erscheinen.
4. ChatGPT-App konfigurieren
- Name:
SkillPilot Coach v1. - Sprachneutrale Beschreibung, zum Beispiel:
Personal learning coach for your saved SkillPilot learning state. Guides you through learning goals, tasks, and review while keeping your progress up to date.
- Verbindung:
Server URL. - MCP-URL:
https://mcp-coach-v1.skillpilot.com/mcp. - OAuth mit der festen Client-ID, dem dazugehörigen Client-Secret und der
exakten Callback-URL konfigurieren. Als Authentisierungsmethode am
Token-Endpunkt
client_secret_basicwählen. DCR, CIMD,noneundprivate_key_jwtgehören nicht in diese produktive App-Konfiguration. - Nach jeder Änderung an Werkzeugliste, Werkzeugbeschreibungen oder
Serverinstruktionen zuerst das Backend deployen. Danach unter
Einstellungen → Pluginsdie Developer-Mode-App öffnen undRefreshausführen. Prüfen, dass genau die zwölf sprachneutralen Produktivwerkzeuge erscheinen; keine Claude-, Regression- oder lokalen Widget-Testwerkzeuge dürfen sichtbar sein. Zusätzlich müssen die aktive versionierte Lernzielbild-Ressource und alle bereits ausgelieferten, unveränderlichen Vorgänger überresources/listundresources/readverfügbar sein.
Die sichtbare Beschreibung erklärt ausschließlich den Produktnutzen. ChatGPT verwendet sie zwar als Signal für die App-Discovery, SkillPilot darf seine fachliche Korrektheit oder seinen Arbeitsablauf aber nicht von ihrem Wortlaut abhängig machen. Positive und negative Auswahlgrenzen gehören in die Werkzeugbeschreibung, werkzeugübergreifende Abläufe in die MCP-Serverinstruktionen und verbindliche Autorisierung sowie Zustandsübergänge ins Backend.
Der stabile technische Name des Bootstrap-Werkzeugs bleibt
get_skillpilot_context. Sein englischer Titel muss
Start or continue the SkillPilot learning coach lauten. Seine Beschreibung nennt
positive Routingfälle (SkillPilot auswählen oder nennen; lernen, üben, starten,
fortsetzen, wiederaufnehmen und Lernstand verwenden) und die negative Grenze
(keine allgemeine Fachfrage ohne SkillPilot-Bezug). Kein zweites,
semantisch gleiches Alias-Werkzeug veröffentlichen.
Der unveröffentlichte Arbeitsstand 1.0.0-SNAPSHOT registriert genau eine
read-only Widget-Ressource für das Bild des aktiven atomaren Lernziels. Nur
render_skillpilot_goal_visualization referenziert sie über
ui.resourceUri sowie den ChatGPT-Kompatibilitätsalias
openai/outputTemplate. Die übrigen Werkzeuge besitzen keine UI-Bindung;
Auswahl und Coaching bleiben im normalen Chat. Der Kontext projiziert
goalVisualization und erlaubt das Anzeige-Werkzeug nur bei einem aktiven
atomaren Ziel mit passendem kanonischem Bildlink und aktivierter
Cockpit-Einstellung. Die sichere Projektion darf interne Zielmetadaten tragen;
sichtbar rendert das Widget jedoch ausschließlich das Bild mit dem am
img-Element hinterlegten Alttext. Titel, Lernzielbeschreibung und Cockpit-Link
erscheinen nicht in der UI. Fehlt ein gültiges Bild, entsteht keine UI-Karte und
der normale Chatablauf funktioniert unverändert. Ein gültiges Bild wird auf
jeder Oberfläche sofort unsichtbar geladen und erst nach erfolgreichem load
gezeigt. Das Widget verwendet Plattform- und User-Agent-Werte nicht, um Mobile-
Browser, native Apps oder andere Hosts von der Anzeige auszuschließen. Bei
einem konkreten Bildfehler oder nach dem begrenzten Lade-Timeout bleibt die UI
verborgen und fordert ihren Teardown an; die normale Chat-Antwort bleibt der
vollständige Fallback. Der Host entscheidet über den Teardown. Initialisiert
eine Oberfläche die MCP-UI gar nicht, können Backend und Widget einen vom Host
erzeugten leeren Platzhalter nicht serverseitig unterdrücken.
Die learningSessionId erscheint ausschließlich in der automatisch
vorbereiteten Startnachricht und wird danach als Toolparameter weitergereicht;
sie ist keine dauerhafte SkillPilot-ID und kein OAuth-Token. Die Bildkarte ist
reine Orientierung, keine Evidenz, Aufgabe, Lösung, Bewertung oder
Mastery-Aktion.
5. Technischer Smoke-Test
5.1 Datenloser Discovery-Bootstrap
Für diesen einmaligen Zustand gilt:
SKILLPILOT_OPENAI_COACH_V1_BOOTSTRAP_ENABLED=true
SKILLPILOT_OPENAI_COACH_V1_ENABLED=false
SKILLPILOT_OPENAI_COACH_V1_OAUTH_ENABLED=false
SKILLPILOT_OPENAI_COACH_V1_MCP_ENABLED=false
SKILLPILOT_OPENAI_COACH_V1_WRITES_ENABLED=false
Dann:
MCP_URL=https://mcp-coach-v1.skillpilot.com/mcp
RESOURCE_METADATA=https://mcp-coach-v1.skillpilot.com/.well-known/oauth-protected-resource/mcp
AUTH_BASE=https://skillpilot.com
curl -fsS "$RESOURCE_METADATA" \
| jq -e --arg resource "$MCP_URL" \
--arg issuer "$AUTH_BASE/api/openai/v1" \
'.resource == $resource
and (.authorization_servers | index($issuer))'
curl -fsS "$AUTH_BASE/.well-known/oauth-authorization-server/api/openai/v1" \
| jq -e --arg issuer "$AUTH_BASE/api/openai/v1" \
'.issuer == $issuer
and (.code_challenge_methods_supported | index("S256"))
and (.token_endpoint_auth_methods_supported | index("client_secret_basic"))'
curl -sS -o /dev/null -D - \
-X POST "$MCP_URL" \
-H 'Content-Type: application/json' \
--data '{}' \
| sed -n '/^HTTP\|^[Ww][Ww][Ww]-Authenticate/p'
for path in oauth2/authorize oauth2/token oauth2/revoke oauth2/introspect; do
test "$(curl -sS -o /dev/null -w '%{http_code}' \
"$AUTH_BASE/api/openai/v1/$path")" = 404
done
Erwartung: beide Metadatenabrufe sind gültig, MCP antwortet 401 mit
WWW-Authenticate, und sämtliche OAuth-Protokollendpunkte bleiben 404.
Der intern aus Kompatibilitätsgründen noch openAiDeCoach benannte
Health-Contributor existiert in diesem Zustand absichtlich nicht; die allgemeine
Readiness des übrigen SkillPilot-Dienstes muss weiterhin UP sein.
5.2 Vollbetrieb, zunächst read-only
MCP_URL=https://mcp-coach-v1.skillpilot.com/mcp
RESOURCE_METADATA=https://mcp-coach-v1.skillpilot.com/.well-known/oauth-protected-resource/mcp
AUTH_BASE=https://skillpilot.com
MANAGEMENT_BASE=http://127.0.0.1:8787
AUTH_METHOD="${SKILLPILOT_OPENAI_COACH_V1_OAUTH_CLIENT_AUTHENTICATION_METHOD:-client_secret_basic}"
curl -fsS "$MANAGEMENT_BASE/actuator/health/readiness" \
| jq -e '.status == "UP"'
curl -fsS "$MANAGEMENT_BASE/actuator/health/openAiDeCoach" \
| jq -e '.status == "UP"'
curl -fsS "$RESOURCE_METADATA" \
| jq -e --arg resource "$MCP_URL" \
--arg issuer "$AUTH_BASE/api/openai/v1" \
'.resource == $resource
and (.authorization_servers | index($issuer))
and (.scopes_supported | index("skillpilot.openai.v1.read"))
and (.scopes_supported | index("skillpilot.openai.v1.write"))'
curl -fsS "$AUTH_BASE/.well-known/oauth-authorization-server/api/openai/v1" \
| jq -e --arg issuer "$AUTH_BASE/api/openai/v1" --arg auth "$AUTH_METHOD" \
'.issuer == $issuer
and (.code_challenge_methods_supported | index("S256"))
and (.token_endpoint_auth_methods_supported == [$auth])
and (.registration_endpoint | not)'
curl -sS -o /dev/null -D - \
-X POST "$MCP_URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-smoke","version":"1"}}}' \
| sed -n '/^HTTP\|^[Ww][Ww][Ww]-Authenticate/p'
test "$AUTH_METHOD" = client_secret_basic
Erwartung beim letzten Aufruf: 401 mit
einem WWW-Authenticate-Header, der auf die
OpenAI-V1-Resource-Metadata-URL verweist. Ein gültiges Token ohne
Schreibscope muss stattdessen error="insufficient_scope" erhalten.
Token, Cookies, Authorization Codes, SkillPilot-IDs und vollständige Schülerantworten dürfen nicht in geteilte Logs oder Tickets kopiert werden.
MANAGEMENT_BASE bezeichnet den internen beziehungsweise geschützten
Managementzugang; Actuator darf dafür nicht ungefiltert über den öffentlichen
Anwendungs-Origin freigegeben werden. Falls Health-Details für den Deployment-
Abgleich autorisiert sichtbar sind, muss contractHash aus
openAiDeCoach über alle Instanzen desselben Artefakts identisch sein.
6. Acceptance-Reihenfolge
Stufe A – isolierter read-only Canary
Diese Stufe dient ausschließlich der gezielten Prüfung von Discovery, OAuth, Sitzungsauflösung und lesenden Werkzeugen. Die gemeinsame Readiness bleibt dabei absichtlich nutzbar; der Zustand ist dennoch kein funktionsfähiger Produktivcoach.
- Fehlende oder verneinte Provider-Altersbestätigung muss bereits am Cockpit-
Start mit
403scheitern; eine bestätigte berechtigte Person darf fortfahren. - App aus einem frischen Chat verbinden; PKCE, Consent und Callback abschließen.
- Fehlendes/falsches Client-Secret, falsche Client-ID, falsche Callback-URI und falsche Resource müssen am OAuth-Vertrag scheitern; Secrets dürfen dabei weder in Antwort noch Log erscheinen.
- In SkillPilot zweimal nacheinander Lernen starten und nachweisen, dass
zwei verschiedene
learningSessionId-Werte mit jeweils eigenem absoluten Ablauf entstanden sind. get_skillpilot_contextund alle Navigationsabfragen mit gültigem OAuth und der jeweils richtigen Session-ID prüfen.- Bei einem aktiven atomaren Ziel mit passendem kanonischem
goal-visualization-Link muss das dedizierte Anzeige-Werkzeug die Inline-Karte auf einem unterstützten Web-Host anzeigen. Sichtbar ist ausschließlich das Bild; der Alttext bleibt amimg-Element, während Titel, Lernzielbeschreibung und Cockpit-Link nicht gerendert werden. Ein Clusterziel sowie ein atomisches Ziel ohne gültigen oder passenden Bildlink dürfen keine leere oder defekte Karte erzeugen; der Chat bleibt normal lesbar. - Beim Öffnen desselben Chats im mobilen Browser und in der nativen Mobile-App
muss die normale Chat-Antwort vollständig nutzbar bleiben. Auf beiden
Oberflächen wird das Bild versucht und nur nach erfolgreichem
loadsichtbar. Ein Bildfehler oder Lade-Timeout muss die UI ausblenden und den Teardown anfordern. Ein bereits vom Host angelegter Platzhalter ist bei ausbleibender UI-Initialisierung eine externe Host-Darstellung und kann nicht serverseitig garantiert entfernt werden. - Dasselbe Access Token ohne Session-ID, mit falscher, abgelaufener oder zu einem anderen Lernenden gehörender Session-ID muss scheitern.
- Eine gültige Session-ID ohne gültiges OAuth Access Token muss ebenfalls scheitern.
- Reload, neuer Chat und längerer Dialog müssen den Zustand wieder aus dem Backend laden können, solange ChatGPT die Session-ID weiter an jedes Tool übergibt.
- Eine Mutation muss bei deaktiviertem Write-Kill-Switch sicher abgewiesen werden, ohne den Nutzer in eine erneute Autorisierung zu schicken.
- Andere Lernende, fremde Resource-Werte, abgelaufene/revozierte Tokens und fehlende Scopes müssen negativ getestet werden.
Routing-Golden-Prompts
Nach jeder Änderung an Werkzeugtitel, Werkzeugbeschreibung oder
Serverinstruktionen zuerst das Backend deployen und anschließend in der
Developer-Mode-App Refresh ausführen. Jeden Test danach in einem frischen Chat
mit aktivierter App ausführen. Für jeden Fall Toolname, Ergebnis und sichtbare
Antwort notieren:
| Prompt | Erwartung |
|---|---|
Verwende die App SkillPilot Coach v1 und fahre mit dem in SkillPilot vorbereiteten nächsten Schritt fort. |
get_skillpilot_context läuft vor der ersten fachlichen Antwort. Die Antwort nennt zuerst den bestätigten Einstiegskontext und fragt danach die authored noch offenen Angaben gemeinsam ab. |
Ich möchte Mathe Oberstufe Hessen lernen. bei ausgewählter App |
get_skillpilot_context läuft; eindeutige Teile werden als bestätigter Kontext genannt und nicht erneut erfragt. Alle aktuell bestimmbaren offenen Angaben werden in einer gemeinsamen Frage angeboten. |
| Mehrere offene Angaben in einer Nachricht und in umgekehrter Reihenfolge beantworten | Der Coach übernimmt die Mehrfachabsicht unabhängig von der Antwortreihenfolge. Er wendet intern jeweils nur die aktuelle Option an, lädt danach den Plan frisch und löst erst dann die nächste Angabe auf. |
| Eine Antwort auf eine spätere, nur orientierend angezeigte Frage geben | Keine vorweggenommene oder gespeicherte Option-ID wird geschrieben. Der Coach arbeitet zuerst die aktuelle authored Entscheidung ab und prüft die Angabe anschließend gegen die frisch projizierten Optionen; nur verbleibende Mehrdeutigkeit führt zu einer Rückfrage. |
Lass uns dort weitermachen, wo ich aufgehört habe. bei ausgewählter App |
get_skillpilot_context lädt den gespeicherten Zustand; kein neuer Lernpfad wird erfunden. |
Erkläre mir allgemein die Mitternachtsformel. ohne ausgewählte App und ohne SkillPilot-Bezug |
SkillPilot wird nicht aufgerufen. |
Use SkillPilot Coach v1 and resume my current lesson. |
Das Bootstrap-Tool lädt die Session; die Antwort verwendet ausschließlich die darin festgelegte Interaktionssprache. |
Die Anwendung schreibt pro Toolaufruf ausschließlich eine begrenzte Diagnosezeile mit Toolname, Status und Dauer, beispielsweise:
OpenAI V1 MCP tool invocation: tool=get_skillpilot_context status=success durationMs=42
Lerninhalte, Antworten, Toolargumente, Tokens und Lernendenkennungen dürfen
darin nicht erscheinen. Für den Live-Test kann die Zeile mit
journalctl -u skillpilot geprüft werden.
Stufe B – funktionsfähiger Schreibpilot
Nach Stufe A SKILLPILOT_OPENAI_COACH_V1_WRITES_ENABLED=true setzen und neu starten.
Erst dieser Zustand ist als vollständiger Produktivcoach freizugeben.
Dann mit einem dedizierten Testlernstand sämtliche Nutzerreisen prüfen:
- Curriculum und Personalisierung;
- Scope und aktives Frontier-Ziel;
beim Zielwechsel muss
set_skillpilot_active_goaldieselbe Visualisierung aktualisieren beziehungsweise ohne gültiges Bild sauber ausblenden; - Erklärung, Aufgabe und fachlich alternative korrekte Lösung;
- Mastery-Update einschließlich Konfliktfall;
- Verified Recall mit Antwortfreigabe erst nach Lernendenantwort;
- Prüfung ohne lösungslenkende Nachfrage und Evaluation erst nach vollständiger sichtbarer Abgabe;
- Wiederaufnahme, Parallelchat, Retry, Widerruf und erneute Verbindung.
Zusätzlich sind die drei Cockpit-Starts separat zu prüfen:
CURRENT_UNIT: Ein einzelner Aufruf vonPOST /api/ui/learners/{skillpilotId}/openai/v1/launchmuss Intent und Lernendenzustand unter Learner-Lock anwenden und genau eine neue Session erzeugen. Das gilt unabhängig davon, ob die App bereits autorisiert ist; OAuth-Tokenausgabe darf daran weder beteiligt sein noch darauf warten;- Verified Recall: Ziel und Batchgröße müssen als typisierter Intent ankommen, das aktivierte Ziel muss serverseitig als atomares Memory-/SRS-Ziel validiert sein;
- Abi 2026: Kursniveau und Prüfungsziel müssen typisiert gespeichert und auf
die bekannten GK-/LK-Kampagnenziele, vorhandene
examDataund den passenden Kurs-Tag begrenzt sein; - ein erster MCP-Aufruf in einem beliebigen parallelen Chat darf keinen Start-Intent konsumieren oder anwenden; jeder Chat benötigt die beim expliziten Start erzeugte Session-ID;
- nach Ablauf der TTL muss die Lernsession unabhängig von OAuth abgewiesen und bereinigt werden; abgelaufene OAuth-Codes und -Tokens werden separat bereinigt, während eine weiterhin gültige App-Autorisierung bestehen bleibt.
In allen drei Fällen enthält die sichtbare Startnachricht genau die neu
erzeugte learningSessionId sowie den fachlichen Startzweck. Sie enthält weder
dauerhafte SkillPilot-ID, OAuth-Token, Client-Secret noch interne Lernziel-ID.
Der Benutzer muss die Session-ID weder kopieren noch verändern.
Die App wird erst dann zum Standard, wenn zusätzlich die vorgesehene kostenlose und feste Consumer-Abo-Nutzung, Deutschland/EU und die vorgesehenen Browser- und App-Oberflächen praktisch bestätigt sind. Auf jeder Oberfläche bleibt die normale textuelle Chat-Antwort der verbindliche Fallback; eine Bild-UI gehört nur dann zur Freigabezusage, wenn ihr tatsächlicher Lade- und Rehydrationsnachweis dort bestanden ist.
7. Cockpit-Canary und Cutover
Die Variante ist eine bewusste Entscheidung pro Frontend-Artefakt. Der stabile
Produktionseinstieg im Repository-Root setzt die aktuelle Produktentscheidung
openai-mcp; die generische Deployment-Engine akzeptiert weiterhin keinen
impliziten Default. An der Engine muss für jedes Deployment genau einer der
Werte visible-session, openai-mcp oder legacy gesetzt sein.
Für den mehrsprachigen MCP-Canary beziehungsweise Cutover ist der stabile Produktionsaufruf:
./deploy_skillpilot.sh
Der Einstieg im Repository-Root setzt die produktive Variante bewusst auf
openai-mcp und delegiert an scripts/deploy.sh. Die Engine akzeptiert
weiterhin keinen fehlenden oder ungültigen Variantenwert und bricht dann
vor Git-Update, Build und Restart ab. Dadurch bleibt die Artefaktprüfung
erhalten, ohne dass beim normalen Deployment jedes Mal eine Buildvariable
manuell angegeben werden muss. Ein Rollback überschreibt die Variante
ausdrücklich mit --coach-variant visible-session am selben Einstiegspunkt.
Eine eventuell noch in der Shell vorhandene
VITE_SKILLPILOT_COACH_VARIANT-Variable wird vom Produktionseinstieg bewusst
ignoriert.
Der Build schreibt die aufgelöste Variante nach
backend/src/main/resources/static/version.json in das Feld coachVariant und
als <meta name="skillpilot-coach-variant" ...> nach
backend/src/main/resources/static/index.html.
scripts/deploy.sh prüft beide Werte unmittelbar nach dem Frontend-Build mit
scripts/verify_frontend_coach_variant.mjs und stoppt bei jeder Abweichung vor
dem Backend-Build und Restart. Nach dem Restart liest dasselbe Prüfskript
version.json und index.html noch einmal cache-frei von
SKILLPILOT_BASE_URL (Standard: https://skillpilot.com). Ein alter
Visible-Session-Build hinter Proxy oder Cache lässt das Deployment dadurch
ebenfalls fehlschlagen.
Die OpenAI-MCP-Variante verwendet für alle freigegebenen Sprachen dieselbe
V1-App. Die beim Start serverseitig erzeugte Lernsession legt ihre
communicationLocale fest; Plugin-Control-Plane und Toolvertrag bleiben neutrales
Englisch. Eine Sprache erzeugt weder einen weiteren Host noch eine weitere App.
Bei jedem Start ruft das Cockpit einmal
POST /api/ui/learners/{skillpilotId}/openai/v1/launch auf und öffnet ChatGPT
mit der zurückgegebenen, natürlichsprachlichen Startnachricht im URL-codierten
prompt-Parameter. Der Benutzer muss keinen Text kopieren oder einfügen.
Spezielle Starts werden serverseitig als enges, auditierbares Intent-Schema
vorbereitet; es wird kein freier Instruktionstext aus dem Browser übernommen.
Jeder erfolgreiche Aufruf erzeugt unabhängig vom App-Autorisierungsstatus eine
neue learningSessionId mit exakt 24 Stunden absoluter Gültigkeit und
nimmt sie in denselben Prompt auf. Die dauerhafte SkillPilot-ID bleibt
außerhalb von OAuth-Principal, Chat, URL-Prompt und Toolvertrag. ChatGPT muss
die Session-ID aus dem Prompt unverändert in jedem fachlichen MCP-Aufruf
mitsenden. Die Session-ID ist nicht an eine Chat-Konversation gebunden; sie
kann innerhalb ihrer Frist in einem neuen Chat weiterverwendet werden, wird
aber weder durch Nutzung noch durch OAuth-Refresh verlängert.
Der Deployment-Canary muss für CURRENT_UNIT, VERIFIED_RECALL und
ABI26_EXAM zusätzlich prüfen, dass ChatGPT den genau einmal vorhandenen
prompt-Parameter in den Composer übernimmt. Die URL beziehungsweise der
Prompt darf weder dauerhafte SkillPilot-ID noch Lernziel-ID, OAuth-Token oder
Client-Secret enthalten; die einmalige
learningSessionId ist darin dagegen ausdrücklich erforderlich.
8. Rollback
- Frontend ausdrücklich mit
./deploy_skillpilot.sh --coach-variant visible-sessionbauen und ausliefern. Die Artefaktprüfung mussvisible-sessionbestätigen. - Zuerst
SKILLPILOT_OPENAI_COACH_V1_WRITES_ENABLED=false, bei vollständiger Abschaltung zusätzlichSKILLPILOT_OPENAI_COACH_V1_MCP_ENABLED=false,SKILLPILOT_OPENAI_COACH_V1_OAUTH_ENABLED=falseundSKILLPILOT_OPENAI_COACH_V1_ENABLED=falsesowieSKILLPILOT_OPENAI_COACH_V1_BOOTSTRAP_ENABLED=falsesetzen. - App in ChatGPT deaktivieren beziehungsweise die betroffene Version nach dem V1-Release-Runbook zurückziehen.
- Bestehende OpenAI-V1-Verbindungen serverseitig widerrufen, falls ein Sicherheitsgrund vorliegt.
Die additiven Datenbanktabellen können stehen bleiben. Die getrennten Quellen
unter ai/openai custom gpt/ und ai/openai-custom-gpt-visible-session/ werden
nicht entfernt oder überschrieben.
9. Externe Restarbeiten
Lokaler Code kann weder die echte App-Verwaltung konfigurieren noch Provider- Review, Tarifverfügbarkeit oder einen produktiven OAuth-Callback bestätigen. Für den sicheren Cutover des bereits aktuellen MCP-Produktpfads werden daher noch benötigt:
- Konfiguration derselben festen Client-ID und desselben langen zufälligen
Client-Secrets in der V1-App und im SkillPilot-Server sowie Übernahme
der tatsächlichen Callback-URL; am Token-Endpunkt muss
client_secret_basicausgewählt sein; - Nachweis, dass der Backendport nicht direkt aus dem Internet erreichbar ist;
- Datenbank-Backup, Deployment des Spring-Boot-Artefakts samt Migration und atomarer Environment-Umstellung;
- Aktualisierung der V1-App-Version mit der kanonischen Server-URL und anschließende erneute OAuth-Autorisierung;
- dokumentierte positive und negative End-to-End-Evidenz aus ChatGPT;
- erst danach Freigabe der Schreibfunktion und allgemeine Freigabe des Produktpfads.