diff --git a/.claude/skills/aeon/references/mcp.md b/.claude/skills/aeon/references/mcp.md index a0e442f51d..a66e7e1fcb 100644 --- a/.claude/skills/aeon/references/mcp.md +++ b/.claude/skills/aeon/references/mcp.md @@ -108,7 +108,7 @@ The script is **sourced, never executed**: it can't abort the run, guards every If a provider rotates the refresh token on each use, the old one dies immediately. Unless the replacement is **saved back**, the *next* run fails with `invalid_grant` — auth breaks one run later, not now. -Writing a secret needs a secrets-write credential, and **the default `GITHUB_TOKEN` cannot do it.** Add a fine-grained PAT with **Secrets: read/write** as **`MCP_SECRETS_PAT`** (or repo-wide `GH_GLOBAL`). +Writing a secret needs a secrets-write credential, and **the default `GITHUB_TOKEN` cannot do it.** Add a fine-grained PAT with **Secrets: read/write** as **`GH_SECRETS_PAT`** (or repo-wide `GH_GLOBAL`). **All four catalog providers rotate — treat the PAT as required, not optional.** diff --git a/.claude/skills/aeon/references/secrets.md b/.claude/skills/aeon/references/secrets.md index e748d51ca8..9fdb2e0eb5 100644 --- a/.claude/skills/aeon/references/secrets.md +++ b/.claude/skills/aeon/references/secrets.md @@ -69,9 +69,9 @@ Telegram is the fastest to set up and the only one with inline buttons and slash | `GITHUB_TOKEN` | Built in — nothing to set. Scoped to this repo only | | `GH_GLOBAL` | github.com/settings/tokens → Fine-grained → select repos → Contents, Pull requests, Issues (read/write). Needed for anything cross-repo (`github-monitor`, `pr-review`, `feature`, `changelog push-to`). Auto-promoted to the run's `GITHUB_TOKEN` | | `GH_READ_PAT` | Same page, read-only. Optional — enriches cross-repo/private reads (`bd-radar`) without granting write | -| `MCP_SECRETS_PAT` | github.com/settings/personal-access-tokens → **add this repo under Repository access** (a PAT without it 404s) → Repository permissions → Secrets: Read and write. Only needed if you use OAuth-connected MCP servers | +| `GH_SECRETS_PAT` | github.com/settings/personal-access-tokens → **add this repo under Repository access** (a PAT without it 404s) → Repository permissions → Secrets: Read and write. Only needed for OAuth-connected MCP servers or the Grok X-account harness | -`MCP_SECRETS_PAT` gotcha: providers rotate the refresh token every run, and the runner needs this PAT to save each rotation back. Without it, auth breaks exactly one run after you connect. After adding it, re-connect any already-connected server once. +`GH_SECRETS_PAT` gotcha: providers rotate the refresh token every run, and the runner needs this PAT to save each rotation back. Without it, auth breaks exactly one run after you connect. After adding it, re-connect any already-connected server once. ## 4. Skill API keys — all optional diff --git a/.github/workflows/aeon.yml b/.github/workflows/aeon.yml index ffd7321aab..ba11efd990 100644 --- a/.github/workflows/aeon.yml +++ b/.github/workflows/aeon.yml @@ -359,7 +359,7 @@ jobs: XAI_API_KEY: ${{ secrets.XAI_API_KEY }} # secrets-write PAT so run-grok.sh setup can PERSIST a rotated grok refresh # token back to GROK_CREDENTIALS - the durable-OAuth-past-6h fix (run-grok.sh §2b). - MCP_SECRETS_PAT: ${{ secrets.MCP_SECRETS_PAT }} + GH_SECRETS_PAT: ${{ secrets.GH_SECRETS_PAT }} GH_GLOBAL: ${{ secrets.GH_GLOBAL }} # Optional: seconds-before-expiry that triggers the grok OAuth refresh # (run-grok.sh §2b, default 1800). Set a large value as a repo variable to diff --git a/.github/workflows/messages.yml b/.github/workflows/messages.yml index 6f039071b2..71c9e1374e 100644 --- a/.github/workflows/messages.yml +++ b/.github/workflows/messages.yml @@ -435,7 +435,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} GROK_CREDENTIALS: ${{ secrets.GROK_CREDENTIALS }} # grok harness: X-account OAuth (restored by run-grok.sh) - MCP_SECRETS_PAT: ${{ secrets.MCP_SECRETS_PAT }} # persist rotated grok refresh token → GROK_CREDENTIALS (run-grok.sh §2b) + GH_SECRETS_PAT: ${{ secrets.GH_SECRETS_PAT }} # persist rotated grok refresh token → GROK_CREDENTIALS (run-grok.sh §2b) GH_GLOBAL: ${{ secrets.GH_GLOBAL }} GROK_OAUTH_SKEW: ${{ vars.GROK_OAUTH_SKEW }} # force grok OAuth refresh + persist every run when set large (durability check) COINGECKO_API_KEY: ${{ secrets.COINGECKO_API_KEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6251dc20dc..1833932398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ from or pin to; the template keeps serving the latest `main` to new forks. on `ETHERSCAN_API_KEY`, the same Etherscan v2 key). (#760) - **Dashboard: per-skill model picker tracks the active harness** — a skill's detail panel now offers the selected harness's model ids. (#766) +- **Secret rename: `MCP_SECRETS_PAT` -> `GH_SECRETS_PAT`.** One standardized + secrets-write PAT now serves both the OAuth MCP refresh and the Grok X-account + refresh (still falling back to `GH_GLOBAL`). Operators using the old name + should re-add the PAT as `GH_SECRETS_PAT`; `GH_GLOBAL` users are unaffected. ### Fixed @@ -47,7 +51,7 @@ from or pin to; the template keeps serving the latest `main` to new forks. (§2b) now refreshes the access token before each run and **persists the rotated `auth.json` back to the `GROK_CREDENTIALS` secret** - the same durable-refresh contract as MCP OAuth. Persisting reuses the secrets-write PAT - (`MCP_SECRETS_PAT` / `GH_GLOBAL`); without it grok warns loudly. Also adds a + (`GH_SECRETS_PAT` / `GH_GLOBAL`); without it grok warns loudly. Also adds a `grok)` case to the harness AUTH_MODE detection so a Connected X account is labelled `native-oauth` instead of defaulting to `openrouter`. See [docs/harnesses.md](docs/harnesses.md) and [docs/mcp-oauth.md](docs/mcp-oauth.md). diff --git a/apps/dashboard/app/page.tsx b/apps/dashboard/app/page.tsx index abaec4a884..bd3be834dd 100644 --- a/apps/dashboard/app/page.tsx +++ b/apps/dashboard/app/page.tsx @@ -286,7 +286,7 @@ export default function Dashboard() { {showImport && setShowImport(false)} onImport={importSkill} />} {showAuthModal && (harness === 'grok' ? setShowAuthModal(false)} onGrokAuth={(p) => setupGrokAuth(p)} - patSet={secrets.some(s => s.isSet && (s.name === 'MCP_SECRETS_PAT' || s.name === 'GH_GLOBAL'))} + patSet={secrets.some(s => s.isSet && (s.name === 'GH_SECRETS_PAT' || s.name === 'GH_GLOBAL'))} onGoToSecret={(n) => { setShowAuthModal(false); goToSecret(n) }} /> : HARNESS_AUTH[harness] ? setShowAuthModal(false)} onHarnessAuth={(p) => setupHarnessAuth(harness, p)} /> diff --git a/apps/dashboard/components/GrokAuthModal.tsx b/apps/dashboard/components/GrokAuthModal.tsx index 904ea73e78..fab87ecaf4 100644 --- a/apps/dashboard/components/GrokAuthModal.tsx +++ b/apps/dashboard/components/GrokAuthModal.tsx @@ -13,7 +13,7 @@ interface GrokAuthModalProps { loading: boolean onClose: () => void onGrokAuth: (payload?: { key: string }) => void - // Whether a secrets-write PAT (MCP_SECRETS_PAT / GH_GLOBAL) is + // Whether a secrets-write PAT (GH_SECRETS_PAT / GH_GLOBAL) is // set - the OAuth session rotates its refresh token and only survives past 6h if // the runner can persist each rotation back to GROK_CREDENTIALS. Hides the note once set. patSet: boolean @@ -38,14 +38,14 @@ export function GrokAuthModal({ loading, onClose, onGrokAuth, patSet, onGoToSecr {/* Secrets-PAT setup note (parallels McpPanel's). The X-account session rotates its refresh token on every run; the runner can only save each rotation back with a secrets-write PAT. Without one, a Connected account - works ~6h and then its auth breaks. Hidden once MCP_SECRETS_PAT / GH_GLOBAL + works ~6h and then its auth breaks. Hidden once GH_SECRETS_PAT / GH_GLOBAL is set. */} {!patSet && (

⚠ The X-account session won't keep working without a secrets PAT

Grok rotates its refresh token on every run, and the runner needs a secrets-write credential to save each rotation - without it a Connected account works once (~6h), then its auth breaks. To set it up: create a fine-grained PAT at github.com/settings/personal-access-tokens, add this repo under Repository access, grant Secrets: Read and write, and save it as{' '} - + {' '}in Settings. Already Connected? Re-connect once after adding the PAT.

diff --git a/apps/dashboard/components/McpPanel.tsx b/apps/dashboard/components/McpPanel.tsx index 3c420f2f59..5d91da62be 100644 --- a/apps/dashboard/components/McpPanel.tsx +++ b/apps/dashboard/components/McpPanel.tsx @@ -237,13 +237,13 @@ export function McpPanel({ harness, servers, loading, saving, secrets, busy, onS on every run; the runner can only save each rotation back if a secrets-write PAT is set. Without one, an OAuth MCP server works for exactly one run after Connect and then its auth breaks. Shown until - MCP_SECRETS_PAT (or repo-wide GH_GLOBAL) exists; hidden once set. */} - {!(isSecretSet('MCP_SECRETS_PAT') || isSecretSet('GH_GLOBAL')) && ( + GH_SECRETS_PAT (or repo-wide GH_GLOBAL) exists; hidden once set. */} + {!(isSecretSet('GH_SECRETS_PAT') || isSecretSet('GH_GLOBAL')) && (

⚠ OAuth MCP servers won't keep working without a secrets PAT

Providers rotate their refresh token on every run, and the runner needs a secrets-write credential to save each rotation — without it a Connected server works once, then its auth breaks. To set it up: create a fine-grained PAT at github.com/settings/personal-access-tokens, add this repo under Repository access, grant Secrets: Read and write, and save it as{' '} - + {' '}in Settings. Already Connected a server? Re-connect it once after adding the PAT.

diff --git a/apps/dashboard/lib/mcp-catalog.ts b/apps/dashboard/lib/mcp-catalog.ts index 29750f343c..c87efb0d8c 100644 --- a/apps/dashboard/lib/mcp-catalog.ts +++ b/apps/dashboard/lib/mcp-catalog.ts @@ -55,7 +55,7 @@ export const MCP_CATALOG: McpCatalogEntry[] = [ // (auth method "none"). Its ONLY advertised scope is "internal" — do NOT request // offline_access here (glim needs it, Robinhood doesn't have it and would reject // it); refresh tokens come from the refresh_token grant by default. Durable - // refresh (rotated-token persistence via MCP_SECRETS_PAT) is handled generically + // refresh (rotated-token persistence via GH_SECRETS_PAT) is handled generically // by scripts/mcp-oauth-refresh.sh — see docs/mcp-oauth.md. oauth: true, }, diff --git a/apps/dashboard/lib/secrets-catalog.ts b/apps/dashboard/lib/secrets-catalog.ts index e8f5ab94d2..e20e221d87 100644 --- a/apps/dashboard/lib/secrets-catalog.ts +++ b/apps/dashboard/lib/secrets-catalog.ts @@ -59,7 +59,7 @@ export const BUILTIN_SECRETS: Omit[] = [ { name: 'ADMANAGE_API_KEY', group: 'Skill Keys', description: 'AdManage API key - ad-campaign skill (schedule-ads). From admanage.ai/api-docs' }, { name: 'GH_GLOBAL', group: 'Skill Keys', description: 'GitHub PAT with cross-repo WRITE access - cross-repo skills & deploys (changelog push-to, feature external, deploy-prototype, vuln-scanner). Auto-promoted to the run\'s GITHUB_TOKEN. Create one at github.com/settings/tokens' }, { name: 'GH_READ_PAT', group: 'Skill Keys', description: 'GitHub read-only PAT - optional. Enriches cross-repo / private-repo reads (bd-radar); kept separate from the write-capable GH_GLOBAL for least privilege. Without it those skills fall back to public data. Create a read-only token at github.com/settings/tokens' }, - { name: 'MCP_SECRETS_PAT', group: 'Skill Keys', description: 'Fine-grained GitHub PAT with Secrets: read/write on THIS repo - keeps OAuth-connected MCP servers working. Providers rotate the refresh token on every run; the runner needs this PAT to save each rotation back, otherwise auth breaks one run after Connect. Create at github.com/settings/personal-access-tokens: add this repo under Repository access (a PAT without it 404s) + Repository permissions > Secrets: Read and write. Then re-connect any already-connected server once' }, + { name: 'GH_SECRETS_PAT', group: 'Skill Keys', description: 'Fine-grained GitHub PAT with Secrets: read/write on THIS repo - keeps OAuth-connected MCP servers and the Grok X-account harness working. Providers rotate the refresh token on every run; the runner needs this PAT to save each rotation back, otherwise auth breaks one run after Connect. Create at github.com/settings/personal-access-tokens: add this repo under Repository access (a PAT without it 404s) + Repository permissions > Secrets: Read and write. Then re-connect any already-connected server once' }, { name: 'BASE_RPC_URL', group: 'Skill Keys', description: 'Custom Base RPC endpoint - onchain Base skills (investigation-report, token-movers). Optional: a public RPC is used by default; set a paid endpoint to lift rate limits. Find a provider at docs.base.org/chain/node-providers' }, ] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 069503adb9..80203a323c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -69,7 +69,7 @@ cp docs/examples/mcp/.mcp.json.example .mcp.json # then edit, commit, push The example ships two working servers — `github` (uses the runner's built-in `GITHUB_TOKEN`) and `sequential-thinking` (no-auth stdio). On the next run the runner loads `.mcp.json` and auto-allows every server's tools, so a skill can just say *"use the github MCP server to …"*. Reference a server's secret with `${VAR}` (never commit the value) and set it in the dashboard — the runner resolves it from the repo's secrets with zero workflow editing, and skips a server (with a warning) when its secret is missing rather than breaking the skill. -Or skip the file entirely: the dashboard's **MCP** tab writes `.mcp.json` for you, lists **Featured** servers ([Base](https://mcp.base.org), [Robinhood Trading](https://agent.robinhood.com), [glim.sh](https://glim.sh), [Executor](https://executor.sh)) for one-click install, and tells you which secret each server needs. The featured servers are OAuth-gated: **Connect** opens your browser to authorize, then keeps the tokens fresh across headless runs — including saving rotated refresh tokens, which needs a secrets-write PAT (`MCP_SECRETS_PAT`). Flow, PAT setup, and limits: [`docs/mcp-oauth.md`](mcp-oauth.md). Each featured server has a matching on-demand skill (`base-mcp`, `robinhood-mcp`, `glim-mcp`, `executor-mcp`) — dispatch it with a `var` to use the server from a run. +Or skip the file entirely: the dashboard's **MCP** tab writes `.mcp.json` for you, lists **Featured** servers ([Base](https://mcp.base.org), [Robinhood Trading](https://agent.robinhood.com), [glim.sh](https://glim.sh), [Executor](https://executor.sh)) for one-click install, and tells you which secret each server needs. The featured servers are OAuth-gated: **Connect** opens your browser to authorize, then keeps the tokens fresh across headless runs — including saving rotated refresh tokens, which needs a secrets-write PAT (`GH_SECRETS_PAT`). Flow, PAT setup, and limits: [`docs/mcp-oauth.md`](mcp-oauth.md). Each featured server has a matching on-demand skill (`base-mcp`, `robinhood-mcp`, `glim-mcp`, `executor-mcp`) — dispatch it with a `var` to use the server from a run. ## Cross-repo access diff --git a/docs/harnesses.md b/docs/harnesses.md index 390c44da43..ebd2d9dc44 100644 --- a/docs/harnesses.md +++ b/docs/harnesses.md @@ -131,7 +131,7 @@ that, `scripts/run-grok.sh` (§2b) refreshes the access token from the refresh t before each run and **persists the rotated `auth.json` back to the `GROK_CREDENTIALS` secret**. Persisting a secret needs a secrets-write credential - the default `GITHUB_TOKEN` cannot - so set a fine-grained PAT with **Secrets: read/write** as -`MCP_SECRETS_PAT` (or `GH_GLOBAL`). Without the PAT, grok +`GH_SECRETS_PAT` (or `GH_GLOBAL`). Without the PAT, grok warns loudly and auth breaks one run after the first post-expiry refresh. **After adding the PAT, re-connect the X account once** to seed a valid refresh token (a token already consumed by a prior run can't be revived by the PAT alone). Concurrent grok diff --git a/docs/mcp-oauth.md b/docs/mcp-oauth.md index 22c4a83494..5460065279 100644 --- a/docs/mcp-oauth.md +++ b/docs/mcp-oauth.md @@ -52,7 +52,7 @@ discovery notes) and their companion skills: The glim and Executor loops were verified end-to-end on a live instance (Connect → per-run refresh → PAT-persisted rotation → refresh off the persisted -token, no re-connect): every catalog provider rotates, so treat `MCP_SECRETS_PAT` +token, no re-connect): every catalog provider rotates, so treat `GH_SECRETS_PAT` as **required** in practice, not optional. ## Code map @@ -80,7 +80,7 @@ as **required** in practice, not optional. and auth breaks one run later. Persisting a secret needs a **secrets-write credential** — the default `GITHUB_TOKEN` cannot write secrets. To make refresh durable for rotating providers, add a fine-grained PAT with **Secrets: read/write** - on this repo as the secret **`MCP_SECRETS_PAT`** (or a repo-wide `GH_GLOBAL`); + on this repo as the secret **`GH_SECRETS_PAT`** (or a repo-wide `GH_GLOBAL`); `scripts/mcp-oauth-refresh.sh` then saves each rotated refresh token back to its `MCP__OAUTH` secret and warns loudly when it can't. **After adding the PAT, re-connect the affected server once** to seed a valid refresh token — a refresh diff --git a/scripts/mcp-oauth-refresh.sh b/scripts/mcp-oauth-refresh.sh index 8ee4c95a2e..2ab6ab25d1 100755 --- a/scripts/mcp-oauth-refresh.sh +++ b/scripts/mcp-oauth-refresh.sh @@ -65,7 +65,7 @@ _mcp_oauth_refresh_one() { # note below). Never echoes the token itself. local oerr oerr=$(jq -r '[.error, .error_description] | map(select(. != null and . != "")) | join(": ")' <<<"$resp" 2>/dev/null) - echo "::warning::MCP OAuth: refresh failed for $oauth_var${oerr:+ ($oerr)} — the stored refresh token was likely rotated/consumed by an earlier run and not saved. Re-connect it in the dashboard, and set a secrets-write PAT (MCP_SECRETS_PAT) so future rotations persist. See docs/mcp-oauth.md." + echo "::warning::MCP OAuth: refresh failed for $oauth_var${oerr:+ ($oerr)} — the stored refresh token was likely rotated/consumed by an earlier run and not saved. Re-connect it in the dashboard, and set a secrets-write PAT (GH_SECRETS_PAT) so future rotations persist. See docs/mcp-oauth.md." return 0 fi @@ -78,7 +78,7 @@ _mcp_oauth_refresh_one() { # so unless the replacement is saved, the next headless run's refresh fails ("no # access_token"). Writing a secret needs a secrets-write credential — the default # GITHUB_TOKEN CANNOT do this — so persistence uses the PAT the caller resolved - # (MCP_SECRETS_PAT / GH_GLOBAL). Failures here are LOUD (::warning::), not silent: + # (GH_SECRETS_PAT / GH_GLOBAL). Failures here are LOUD (::warning::), not silent: # an unpersisted rotation is exactly what silently breaks auth one run later. new_rt=$(jq -r '.refresh_token // empty' <<<"$resp" 2>/dev/null) if [ -n "$new_rt" ] && [ "$new_rt" != "$rt" ]; then @@ -87,13 +87,13 @@ _mcp_oauth_refresh_one() { if [ -z "$updated" ]; then echo "::warning::MCP OAuth: $oauth_var rotated its refresh token but the updated secret JSON could not be built — re-connect it in the dashboard." elif [ -z "$pat" ]; then - echo "::warning::MCP OAuth: $oauth_var uses a ROTATING refresh token but no secrets-write credential is set, so the rotated token cannot be saved and the NEXT run's refresh WILL fail. Add a fine-grained PAT (Secrets: read/write) as repo secret MCP_SECRETS_PAT (or GH_GLOBAL), then re-connect $oauth_var in the dashboard." + echo "::warning::MCP OAuth: $oauth_var uses a ROTATING refresh token but no secrets-write credential is set, so the rotated token cannot be saved and the NEXT run's refresh WILL fail. Add a fine-grained PAT (Secrets: read/write) as repo secret GH_SECRETS_PAT (or GH_GLOBAL), then re-connect $oauth_var in the dashboard." elif ! command -v gh >/dev/null 2>&1; then echo "::warning::MCP OAuth: $oauth_var rotated its refresh token but 'gh' is unavailable to persist it." elif printf '%s' "$updated" | GH_TOKEN="$pat" gh secret set "$oauth_var" >/dev/null 2>&1; then echo "MCP OAuth: persisted rotated refresh token for $oauth_var (durable refresh active)" else - echo "::warning::MCP OAuth: $oauth_var rotated its refresh token but persisting it FAILED — MCP_SECRETS_PAT/GH_GLOBAL needs 'Secrets: read/write' on this repo. Re-connect in the dashboard once fixed." + echo "::warning::MCP OAuth: $oauth_var rotated its refresh token but persisting it FAILED — GH_SECRETS_PAT/GH_GLOBAL needs 'Secrets: read/write' on this repo. Re-connect in the dashboard once fixed." fi fi return 0 @@ -111,10 +111,10 @@ mcp_oauth_refresh() { names=$(jq -r 'keys[] | select(startswith("MCP_") and endswith("_OAUTH"))' <<<"$secrets" 2>/dev/null) [ -z "$names" ] && return 0 # Secrets-write credential used to persist ROTATED refresh tokens (see the - # persistence note in _mcp_oauth_refresh_one). A dedicated MCP_SECRETS_PAT wins; + # persistence note in _mcp_oauth_refresh_one). A dedicated GH_SECRETS_PAT wins; # GH_GLOBAL is the repo-wide fallback. Masked so it never lands in a log. local pat - pat=$(jq -r '.MCP_SECRETS_PAT // .GH_GLOBAL // empty' <<<"$secrets" 2>/dev/null) + pat=$(jq -r '.GH_SECRETS_PAT // .GH_GLOBAL // empty' <<<"$secrets" 2>/dev/null) [ -n "$pat" ] && echo "::add-mask::$pat" local n json while IFS= read -r n; do diff --git a/scripts/run-grok.sh b/scripts/run-grok.sh index c8bfca4684..fe79ad81ad 100755 --- a/scripts/run-grok.sh +++ b/scripts/run-grok.sh @@ -26,7 +26,7 @@ # GROK_CREDENTIALS base64 of the X-account OAuth session captured by the # dashboard (a tar rooted at $HOME, or a single cred file) # GROK_CREDENTIALS_PATH single-file restore target (default ~/.grok/credentials.json) -# MCP_SECRETS_PAT secrets-write PAT (or GH_GLOBAL as the repo-wide fallback) +# GH_SECRETS_PAT secrets-write PAT (or GH_GLOBAL as the repo-wide fallback) # used to PERSIST a rotated refresh token back to the # GROK_CREDENTIALS secret so OAuth auth survives past 6h (§2b); # without it a rotating provider breaks one run after expiry. @@ -121,7 +121,7 @@ fi # only to the ephemeral runner $HOME - the secret still holds the now-revoked token, # and every later run dies "Not signed in". Fix (mirrors scripts/mcp-oauth-refresh.sh): # refresh HERE from the refresh token, rewrite auth.json, then PERSIST the rotated -# file back to the GROK_CREDENTIALS secret via a secrets-write PAT (MCP_SECRETS_PAT / +# file back to the GROK_CREDENTIALS secret via a secrets-write PAT (GH_SECRETS_PAT / # GH_GLOBAL - the default GITHUB_TOKEN cannot write secrets). Only # refreshes when the access token is within GROK_OAUTH_SKEW seconds of expiry, so runs # inside the window reuse the on-disk token and do not needlessly rotate (fewer @@ -166,7 +166,7 @@ grok_oauth_refresh() { access=$(jq -r '.access_token // empty' <<<"$resp" 2>/dev/null) if [ -z "$access" ]; then local oerr; oerr=$(jq -r '[.error,.error_description]|map(select(.!=null and .!=""))|join(": ")' <<<"$resp" 2>/dev/null) - log "::warning::grok oauth: refresh failed${oerr:+ ($oerr)} - the stored refresh token was likely rotated/consumed by an earlier run and not saved. Re-connect the X account in the dashboard, and set a secrets-write PAT (MCP_SECRETS_PAT / GH_GLOBAL) so future rotations persist. See docs/harnesses.md." + log "::warning::grok oauth: refresh failed${oerr:+ ($oerr)} - the stored refresh token was likely rotated/consumed by an earlier run and not saved. Re-connect the X account in the dashboard, and set a secrets-write PAT (GH_SECRETS_PAT / GH_GLOBAL) so future rotations persist. See docs/harnesses.md." return 0 fi new_rt=$(jq -r '.refresh_token // empty' <<<"$resp" 2>/dev/null) @@ -186,15 +186,15 @@ grok_oauth_refresh() { # secrets-write PAT (the default GITHUB_TOKEN cannot). LOUD on failure - an # unpersisted rotation is exactly what breaks auth one run later. if [ -n "$new_rt" ] && [ "$new_rt" != "$rt" ]; then - local pat="${MCP_SECRETS_PAT:-${GH_GLOBAL:-}}" + local pat="${GH_SECRETS_PAT:-${GH_GLOBAL:-}}" if [ -z "$pat" ]; then - log "::warning::grok oauth: refresh token ROTATED but no secrets-write PAT is set, so the rotated token cannot be saved and the NEXT run's refresh WILL fail. Add a fine-grained PAT (Secrets: read/write) as repo secret MCP_SECRETS_PAT (or GH_GLOBAL), then re-connect the X account once." + log "::warning::grok oauth: refresh token ROTATED but no secrets-write PAT is set, so the rotated token cannot be saved and the NEXT run's refresh WILL fail. Add a fine-grained PAT (Secrets: read/write) as repo secret GH_SECRETS_PAT (or GH_GLOBAL), then re-connect the X account once." elif ! command -v gh >/dev/null 2>&1; then log "::warning::grok oauth: refresh token rotated but 'gh' is unavailable to persist GROK_CREDENTIALS." elif tar czf - -C "$HOME" .grok/auth.json 2>/dev/null | base64 | GH_TOKEN="$pat" gh secret set GROK_CREDENTIALS >/dev/null 2>&1; then log "grok oauth: persisted rotated GROK_CREDENTIALS (durable refresh active)" else - log "::warning::grok oauth: refresh token rotated but persisting GROK_CREDENTIALS FAILED - MCP_SECRETS_PAT/GH_GLOBAL needs 'Secrets: read/write' on this repo. Re-connect the X account once fixed." + log "::warning::grok oauth: refresh token rotated but persisting GROK_CREDENTIALS FAILED - GH_SECRETS_PAT/GH_GLOBAL needs 'Secrets: read/write' on this repo. Re-connect the X account once fixed." fi fi } diff --git a/scripts/tests/test_mcp_oauth_refresh.sh b/scripts/tests/test_mcp_oauth_refresh.sh index a7b1dad0d3..f33d36e6bb 100755 --- a/scripts/tests/test_mcp_oauth_refresh.sh +++ b/scripts/tests/test_mcp_oauth_refresh.sh @@ -58,7 +58,7 @@ echo "$out" | grep -qx 'ERREXIT_ON' && pass "errexit restored after sourcing" || # 7. Rotated refresh token + a secrets-write PAT + stubbed gh → token still # exported AND `gh secret set` invoked to persist the new refresh token. -ROT_SECRET='{"MCP_SECRETS_PAT":"pat-xyz","MCP_FOO_OAUTH":"{\"token_endpoint\":\"https://as.example/token\",\"client_id\":\"cid\",\"refresh_token\":\"rt-1\"}"}' +ROT_SECRET='{"GH_SECRETS_PAT":"pat-xyz","MCP_FOO_OAUTH":"{\"token_endpoint\":\"https://as.example/token\",\"client_id\":\"cid\",\"refresh_token\":\"rt-1\"}"}' out=$(ALL_SECRETS="$ROT_SECRET" CURL_OUT='{"access_token":"fresh-9","refresh_token":"rt-2"}' GHMARK="$(mktemp)" bash -c ' curl() { printf "%s" "$CURL_OUT"; } gh() { [ "$1" = "secret" ] && [ "$2" = "set" ] && { echo "GH_TOKEN=$GH_TOKEN NAME=$3" > "$GHMARK"; cat >/dev/null; }; return 0; } diff --git a/scripts/tests/test_run_grok.sh b/scripts/tests/test_run_grok.sh index 51343d9e2f..8d21836886 100755 --- a/scripts/tests/test_run_grok.sh +++ b/scripts/tests/test_run_grok.sh @@ -216,7 +216,7 @@ authf() { jq -r ".[\"https://auth.x.ai::CID\"].$1" "$OHOME/.grok/auth.json"; } # 10a. expired token + PAT → refresh happens AND the rotated secret is persisted CREDS="$(seed_auth "2000-01-01T00:00:00.000000Z")"; GH_LOG="$OHOME/gh.log"; : >"$GH_LOG" -HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" MCP_SECRETS_PAT="pat-test" GH_LOG="$GH_LOG" \ +HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" GH_SECRETS_PAT="pat-test" GH_LOG="$GH_LOG" \ bash "$RABS" setup >/dev/null 2>"$OHOME/e1" { [ "$(authf key)" = "NEWACCESS" ] && [ "$(authf refresh_token)" = "RT1" ]; } \ && pass "oauth: expired token refreshed in auth.json" || bad "oauth refresh (key=$(authf key) rt=$(authf refresh_token); err=$(cat "$OHOME/e1"))" @@ -232,14 +232,14 @@ HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" GH_LOG="$GH_LOG" \ # 10c. token still valid (far-future expiry) → skip refresh, no token-endpoint call CREDS="$(seed_auth "2099-01-01T00:00:00.000000Z")"; CURL_LOG="$OHOME/curl.log"; : >"$CURL_LOG" -HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" MCP_SECRETS_PAT="pat-test" CURL_LOG="$CURL_LOG" \ +HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" GH_SECRETS_PAT="pat-test" CURL_LOG="$CURL_LOG" \ bash "$RABS" setup >/dev/null 2>"$OHOME/e3" { [ "$(authf key)" = "OLDACCESS" ] && [ ! -s "$CURL_LOG" ]; } \ && pass "oauth: valid token → skips refresh" || bad "oauth skip-when-valid (key=$(authf key); curl=$(cat "$CURL_LOG"))" # 10d. token endpoint returns invalid_grant → warn, keep on-disk token, exit 0 CREDS="$(seed_auth "2000-01-01T00:00:00.000000Z")" -HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" MCP_SECRETS_PAT="pat-test" \ +HOME="$OHOME" PATH="$OBIN:$PATH" GROK_CREDENTIALS="$CREDS" GH_SECRETS_PAT="pat-test" \ CURL_FAKE_OUT='{"error":"invalid_grant","error_description":"Refresh token has been revoked"}' \ bash "$RABS" setup >/dev/null 2>"$OHOME/e4"; rc=$? { [ "$rc" = 0 ] && [ "$(authf key)" = "OLDACCESS" ] && grep -q "invalid_grant" "$OHOME/e4"; } \ diff --git a/skills/robinhood-mcp/SKILL.md b/skills/robinhood-mcp/SKILL.md index e3a970d340..58cc374fb0 100644 --- a/skills/robinhood-mcp/SKILL.md +++ b/skills/robinhood-mcp/SKILL.md @@ -18,7 +18,7 @@ Access the operator's **Robinhood Agentic Trading** account through the Robinhoo The server is wired by the dashboard MCP panel's one-click **Connect** (OAuth; tokens stored as `MCP_ROBINHOOD_TRADING_TOKEN` + `MCP_ROBINHOOD_TRADING_OAUTH`, refreshed each run by `scripts/mcp-oauth-refresh.sh`). Its tools surface as `mcp__robinhood-trading__*` — discover them from the server; the tool descriptions are the source of truth, don't assume a fixed list. - **No `mcp__robinhood-trading__*` tool callable** → the server isn't connected (or its secrets are missing, in which case the workflow logged a `::warning::` and skipped MCP). Log `RH_MCP_NOT_CONNECTED`, notify once pointing the operator at the dashboard → MCP → Connect Robinhood Trading, and exit. Don't try to reach the API with curl — there is no static key. -- **Tools exist but return 401/invalid-token** → the OAuth refresh failed (rotating refresh tokens need `MCP_SECRETS_PAT` — see `docs/mcp-oauth.md`). Log `RH_MCP_AUTH_STALE`, notify the operator to re-connect the server once in the dashboard, and exit. Don't retry the same call more than twice. +- **Tools exist but return 401/invalid-token** → the OAuth refresh failed (rotating refresh tokens need `GH_SECRETS_PAT` — see `docs/mcp-oauth.md`). Log `RH_MCP_AUTH_STALE`, notify the operator to re-connect the server once in the dashboard, and exit. Don't retry the same call more than twice. ## Steps