diff --git a/CHANGELOG.md b/CHANGELOG.md index 12edc22..64ee82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ new version heading in the same commit. ## [Unreleased] +## [0.188.0] — 2026-07-14 +### Added +- **Set the GitHub App slug by hand when it can't be auto-detected.** The "Install the App" button needs + the App's slug; normally it's auto-resolved (one-click flow, bot creds via `GET /app`, or a connected + member's install). When none of those apply — e.g. an OAuth-only setup with no bot creds and nobody + installed yet — the **Connections → Creds → GitHub** install section now shows an **App slug** input + (accepts the bare slug or a full `github.com/apps/` URL, normalized) so an admin can enable the + install link directly. Previously there was no way to set it, so the button just never appeared. + (`src/server.ts`, `web/src/App.tsx`, `web/src/lib/api.ts`; `scripts/github-per-member-test.cjs` now 78/78.) + ## [0.187.0] — 2026-07-14 ### Fixed - **The GitHub "Install the App" button/link now shows for hand-configured Apps, regardless of install diff --git a/package-lock.json b/package-lock.json index be0a1a8..9d5336b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.187.0", + "version": "0.188.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.187.0", + "version": "0.188.0", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index aa87678..6fc04b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.187.0", + "version": "0.188.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 c15223d..d31b4fd 100644 --- a/scripts/github-per-member-test.cjs +++ b/scripts/github-per-member-test.cjs @@ -345,6 +345,14 @@ async function main() { 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'); + // Manual slug: an admin can set it directly (bare slug OR a full github.com/apps/ URL) when it + // can't be auto-resolved — so the install button works even with no bot creds + no member install. + gid2.setPrivateKey('', 'owner@test'); osx.settings.setGithubAppSlug('', 'owner@test'); // no bot creds, no slug + let ivm = await (await call('PUT', '/api/settings/integrations', { githubAppSlug: 'https://github.com/apps/my-manual-app' })).json(); + assert((ivm.github.installUrl || '').includes('/apps/my-manual-app/installations/new'), 'manual App slug (from a URL) → install button URL'); + ivm = await (await call('PUT', '/api/settings/integrations', { githubAppSlug: 'Bare-Slug' })).json(); + assert((ivm.github.installUrl || '') === 'https://github.com/apps/bare-slug/installations/new', 'manual bare slug is normalized (lowercased) into the install URL'); + osx.settings.setGithubAppSlug('', 'owner@test'); 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 d89cba4..bd40bda 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3218,6 +3218,13 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req: if (!b.githubClientId.trim()) os.settings.setGithubAppSlug('', me.email); } if (typeof b.githubClientSecret === 'string') new GithubIdentity(os).setClientSecret(b.githubClientSecret, me.email); + // App slug — manual override for the "Install the App" link when it can't be auto-resolved (no bot + // creds + no member installation to read it from). Accept the bare slug or a full github.com/apps/ + // URL; keep only the slug-safe part. + if (typeof b.githubAppSlug === 'string') { + const slug = (b.githubAppSlug.trim().match(/[\w.-]+$/)?.[0] || '').toLowerCase(); + os.settings.setGithubAppSlug(slug, me.email); + } // Company-bot (installation-token) credentials: App ID → setting, RSA private key → vault. When both // are set, pre-warm + VALIDATE by minting a bot token now, so the admin gets immediate feedback and // the first session doesn't have to wait on a cold mint. Audited github.bot_token.minted / .failed. diff --git a/web/src/App.tsx b/web/src/App.tsx index cdfe5c4..861ba11 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10414,6 +10414,7 @@ function IntegrationsSettings({ me }: { me: Member }) { const [ghSecret, setGhSecret] = useState('') const [ghAppId, setGhAppId] = useState('') const [ghPem, setGhPem] = useState('') + const [ghSlug, setGhSlug] = useState('') const [githubFlash, setGithubFlash] = useState<'created' | 'error' | null>(null) const [image, setImage] = useState({ openRouter: false, atlas: false, backend: null, defaultModel: '', configured: false }) const [atKey, setAtKey] = useState('') @@ -10505,12 +10506,12 @@ function IntegrationsSettings({ me }: { me: Member }) { } }, []) - const save = async (body: { composioApiKey?: string; composioWebhookSecret?: string; slackAppToken?: string; slackBotToken?: string; discordBotToken?: string; githubClientId?: string; githubClientSecret?: string; githubAppId?: string; githubPrivateKey?: string; openRouterKey?: string; atlasKey?: string; imageDefaultModel?: string; falKey?: string; videoDefaultModel?: string; chatRouter?: boolean; chatIdleTimeoutMin?: number }, label: string) => { + const save = async (body: { composioApiKey?: string; composioWebhookSecret?: string; slackAppToken?: string; slackBotToken?: string; discordBotToken?: string; githubClientId?: string; githubClientSecret?: string; githubAppId?: string; githubPrivateKey?: string; githubAppSlug?: string; openRouterKey?: string; atlasKey?: string; imageDefaultModel?: string; falKey?: string; videoDefaultModel?: string; chatRouter?: boolean; chatIdleTimeoutMin?: number }, label: string) => { setBusy(true); setHint('') const r = await api.saveIntegrations(body) setBusy(false) if (r.error) return setHint('⚠ ' + r.error) - setKey(''); setWh(''); setAppTok(''); setBotTok(''); setDiscordTok(''); setGhId(''); setGhSecret(''); setGhAppId(''); setGhPem(''); setAtKey(''); setFalKey('') + setKey(''); setWh(''); setAppTok(''); setBotTok(''); setDiscordTok(''); setGhId(''); setGhSecret(''); setGhAppId(''); setGhPem(''); setGhSlug(''); setAtKey(''); setFalKey('') apply(r) setHint(label); setTimeout(() => setHint(''), 1500) // The Socket-Mode / Gateway connection re-dials on the server when tokens change — poll until the @@ -10783,14 +10784,29 @@ function IntegrationsSettings({ me }: { me: Member }) { {/* One-click create is the primary path when nothing's configured yet. */} {!github.configured && } - {/* After creation: the App must be installed on the repos it may touch. */} - {github.installUrl && ( + {/* The App must be installed on the repos it may touch. Show the button when we know the App's + slug (from the one-click flow, the bot creds, or a connected member's install); otherwise let + the admin paste it so the link still works. */} + {github.configured && (
Install the App on your repositories

A GitHub App can only act on repos it's installed on. Grant it your org or specific repos.

- - Install the App ↗ - + {github.installUrl ? ( + + Install the App ↗ + + ) : ( +
+

+ We couldn't auto-detect the App's slug (needs the bot creds below, or a connected install). Paste it to enable the link — + it's the last part of the App's URL, github.com/apps/<slug>. +

+
+ setGhSlug(e.target.value.trim())} placeholder="e.g. agentosiwp" className="h-8 font-mono text-xs" /> + +
+
+ )}
)} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 1d8028e..a02f368 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -1189,7 +1189,7 @@ export const api = { call<{ ok?: boolean; error?: string }>('POST', '/api/connections/disconnect', body), integrations: () => call('GET', '/api/settings/integrations'), atlasModels: () => call<{ configured: boolean; image: { id: string; label: string; priceUsd: number | null }[]; video: { id: string; label: string; priceUsd: number | null }[]; error?: string }>('GET', '/api/integrations/atlas/models'), - saveIntegrations: (body: { composioApiKey?: string; composioWebhookSecret?: string; slackAppToken?: string; slackBotToken?: string; discordBotToken?: string; githubClientId?: string; githubClientSecret?: string; githubAppId?: string; githubPrivateKey?: string; openRouterKey?: string; atlasKey?: string; imageDefaultModel?: string; falKey?: string; videoDefaultModel?: string; chatRouter?: boolean; chatIdleTimeoutMin?: number }) => call('PUT', '/api/settings/integrations', body), + saveIntegrations: (body: { composioApiKey?: string; composioWebhookSecret?: string; slackAppToken?: string; slackBotToken?: string; discordBotToken?: string; githubClientId?: string; githubClientSecret?: string; githubAppId?: string; githubPrivateKey?: string; githubAppSlug?: string; openRouterKey?: string; atlasKey?: string; imageDefaultModel?: string; falKey?: string; videoDefaultModel?: string; chatRouter?: boolean; chatIdleTimeoutMin?: number }) => call('PUT', '/api/settings/integrations', body), // Per-member GitHub (user-to-server OAuth): each member links their OWN account so run-as sessions // push / open PRs as the actual human. `connect` returns the authorize URL to navigate to. githubMe: () => call('GET', '/api/github/me'),