From 4f0bdfd97a85b2a909c5a0220e2bea21ca40726b Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Tue, 14 Jul 2026 14:40:10 +0530 Subject: [PATCH] =?UTF-8?q?fix(github):=20resolve=20App=20slug=20regardles?= =?UTF-8?q?s=20of=20install=20state=20=E2=86=92=20install=20button=20shows?= =?UTF-8?q?=20(v0.187.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The App slug (for the github.com/apps//installations/new link) now resolves from GET /app whenever bot creds are present — not only in the authorized-but-not-installed case. So the Connections → Creds admin install button appears even when already installed (add more orgs/repos), self-healing on any Creds view / GitHub-panel view / bot-creds save. scripts/github-per-member-test.cjs now 76/76. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- scripts/github-per-member-test.cjs | 8 ++++++++ src/server.ts | 11 +++++++++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5badf55..78ff277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ new version heading in the same commit. ## [Unreleased] +## [0.187.0] — 2026-07-14 +### Fixed +- **The GitHub "Install the App" button/link now shows for hand-configured Apps, regardless of install + state.** The App slug (needed to build the `github.com/apps//installations/new` link) is resolved + from `GET /app` whenever the bot creds are present — not only in the authorized-but-not-installed case. + So the **Connections → Creds → GitHub** admin install button appears even when the App is already + installed (for adding more orgs/repos), and it self-heals on any admin Creds view / member GitHub-panel + view / bot-creds save — not just when a member is stuck uninstalled. (`src/server.ts` — resolve in + `GET/PUT /api/settings/integrations` + ungate `/api/github/me`; `scripts/github-per-member-test.cjs` now 76/76.) + ## [0.186.0] — 2026-07-14 ### Added - **`secret_request` — an agent asks a human about a credential KEY, without a paste into the session.** diff --git a/package-lock.json b/package-lock.json index f80063f..be0a1a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.186.0", + "version": "0.187.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.186.0", + "version": "0.187.0", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index 4b7861c..aa87678 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.186.0", + "version": "0.187.0", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/scripts/github-per-member-test.cjs b/scripts/github-per-member-test.cjs index 2233bae..c15223d 100644 --- a/scripts/github-per-member-test.cjs +++ b/scripts/github-per-member-test.cjs @@ -336,7 +336,15 @@ async function main() { const meNI2 = await (await call('GET', '/api/github/me')).json(); assert(meNI2.install && meNI2.install.installed === false && (meNI2.installUrl || '').includes('/apps/agent-os-instapods/installations/new'), '/me self-heals the App slug → a real install link when authorized-but-not-installed'); + // Resolves even when ALREADY installed (so the admin "install on more repos" button is available too). + osx.settings.setGithubAppSlug('', 'owner@test'); installState = 'installed'; + const meInst = await (await call('GET', '/api/github/me')).json(); + assert((meInst.installUrl || '').includes('/apps/agent-os-instapods/installations/new'), 'App slug resolves regardless of install state'); + // And the admin Creds view (GET /api/settings/integrations) self-heals + exposes the install button. + osx.settings.setGithubAppSlug('', 'owner@test'); + const iv2 = await (await call('GET', '/api/settings/integrations')).json(); + assert(iv2.github && (iv2.github.installUrl || '').includes('/apps/agent-os-instapods/installations/new'), 'Creds view resolves the slug → install button URL'); gid2.clear(ownerId); // Removing the private key drops the cached bot token + resolved installation. gid2.setPrivateKey('', 'owner@test'); diff --git a/src/server.ts b/src/server.ts index 09da5b7..d89cba4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3165,6 +3165,10 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: // ── integration credentials (Composio now; Slack app etc. later) — instance-wide secrets ── if (method === 'GET' && p === '/api/settings/integrations') { if (!isAdmin(me)) return sendJson(res, 403, { error: 'owner or admin required' }); + // Self-heal the App slug (→ the Creds "Install the App" button) for a hand-configured App that never + // got one from the manifest flow. Resolves once from GET /app whenever the bot creds are present. + const ghv = new GithubIdentity(os); + if (ghv.botConfigured() && !ghv.appSlug()) await ghv.ensureAppSlug(me.email).catch(() => { /* best-effort */ }); return sendJson(res, 200, integrationsView(os)); } // Live Atlas catalog for the default-model pickers (dropdown + free text). Fetched with the stored @@ -3223,6 +3227,8 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: const ghb = new GithubIdentity(os); if (ghb.botConfigured()) { const bot = await ghb.ensureBotToken(Date.now(), me.email).catch(() => undefined); + // Also resolve the App slug now → the "Install the App" link/button (a hand-set App has no slug yet). + if (!ghb.appSlug()) await ghb.ensureAppSlug(me.email).catch(() => { /* best-effort */ }); os.audit.append({ ts: Date.now(), runId: '-', tenant: os.tenant, principal: me.email, type: bot ? 'github.bot_token.minted' : 'github.bot_token.failed', data: { installationId: os.settings.githubInstallationId() || null } }); } } @@ -3267,8 +3273,9 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: if (st && !('error' in st)) install = st; } // Self-heal the install link: a hand-configured App has no slug from the manifest flow, so resolve it - // once (from GET /app) when we can, so the "Install the App" prompt is a real link, not just text. - if (blob && install && !install.installed && gh.botConfigured() && !gh.appSlug()) { + // once (from GET /app) whenever the bot creds are present — regardless of install state, so the link is + // ready both for the not-installed warning AND the admin's "install on more repos" button. + if (gh.botConfigured() && !gh.appSlug()) { await gh.ensureAppSlug(me.email).catch(() => { /* best-effort; the text guidance still shows */ }); } return sendJson(res, 200, { configured: gh.configured(), connected: !!blob, login: blob?.login, expiresAt: blob?.expiresAt, install, installUrl: gh.installUrl() });