Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug>` 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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 8 additions & 0 deletions scripts/github-per-member-test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug> 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');
Expand Down
7 changes: 7 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug>
// 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.
Expand Down
30 changes: 23 additions & 7 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IntegrationsResp['image']>({ openRouter: false, atlas: false, backend: null, defaultModel: '', configured: false })
const [atKey, setAtKey] = useState('')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -10783,14 +10784,29 @@ function IntegrationsSettings({ me }: { me: Member }) {
{/* One-click create is the primary path when nothing's configured yet. */}
{!github.configured && <GithubSetupGuide />}

{/* 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 && (
<div className="rounded-md border bg-muted/20 p-3">
<div className="text-xs font-medium text-foreground">Install the App on your repositories</div>
<p className="mt-0.5 text-[11px] text-muted-foreground">A GitHub App can only act on repos it's installed on. Grant it your org or specific repos.</p>
<a href={github.installUrl} target="_blank" rel="noreferrer" className={`mt-2 inline-flex ${buttonVariants({ size: 'sm' })}`}>
Install the App ↗
</a>
{github.installUrl ? (
<a href={github.installUrl} target="_blank" rel="noreferrer" className={`mt-2 inline-flex ${buttonVariants({ size: 'sm' })}`}>
Install the App ↗
</a>
) : (
<div className="mt-2 space-y-1.5">
<p className="text-[11px] text-muted-foreground">
We couldn't auto-detect the App's <strong>slug</strong> (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, <code className="text-[11px]">github.com/apps/<strong>&lt;slug&gt;</strong></code>.
</p>
<div className="flex items-center gap-2">
<Input value={ghSlug} onChange={(e) => setGhSlug(e.target.value.trim())} placeholder="e.g. agentosiwp" className="h-8 font-mono text-xs" />
<Button size="sm" onClick={() => save({ githubAppSlug: ghSlug.trim() }, 'saved')} disabled={busy || !ghSlug.trim()}>Set slug</Button>
</div>
</div>
)}
</div>
)}

Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ export const api = {
call<{ ok?: boolean; error?: string }>('POST', '/api/connections/disconnect', body),
integrations: () => call<IntegrationsResp>('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<IntegrationsResp & { ok: boolean }>('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<IntegrationsResp & { ok: boolean }>('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<GithubMe>('GET', '/api/github/me'),
Expand Down
Loading