Skip to content

Commit ea562c9

Browse files
feat(agentos): group-scoped encrypted git PATs + GAP source SHA sync
Let the SDK clone private GAP repos with a group-owned PAT, stored encrypted on the AgentOS server and fetched with the SDK's API key. Server: - crypto/secret-box.ts — AES-256-GCM at rest, keyed by AGENTOS_CREDENTIALS_KEY, with kid-based rotation (AGENTOS_CREDENTIALS_KEY_OLD). Fails closed. - stores/git-credential-store.ts — git_credentials collection, unique (ownerGroup, host) (re-upsert = rotation), normalizeGitHost, resolve(). - routes/git-credentials.ts — CRUD (git-credentials:manage/:read, ownership) + POST /resolve for the SDK's cak_ key (strictly group-scoped, no admin bypass, no-store, token never logged). Mounted in the dashboard router. - permissions + agentos-editor seed; ensureIndexes wiring. - SHA sync: RegistryDoc.sourceSha/sourceSyncedAt, written from session_started payload.agent_sha; drift since the last run is logged. - tests: secret-box + git-credential-store. SPA: - api.gitCredentials client + GitCredential type; Settings -> Git Credentials section (write-only secret) gated on git-credentials:read/:manage. Ops: - scripts/provision-keycloak.mjs (pnpm provision:keycloak) — idempotent realm / roles / OIDC client / mappers / service-account provisioning. - .gitignore: ignore *.env (keycloak.env etc.). - CLAUDE.md: document auth/RBAC (§2.6b), git credentials + SHA sync (§2.6c), new collections, and all new env vars. Note: clone-with-PAT is wired for the SDK LOCAL/library substrate. The remote (sandbox-at-CAS) and AgentOS->CAS dashboard paths reuse CAS's existing gitToken field and are a follow-up.
1 parent 0dc8e78 commit ea562c9

18 files changed

Lines changed: 1388 additions & 6 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ dist/
55
.DS_Store
66
.env
77
.env.local
8+
# Env files holding secrets (e.g. keycloak.env — KC admin pass + client secret).
9+
*.env
810
coverage/
911
.vscode/
1012
.idea/

CLAUDE.md

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,17 @@ Optional GitHub repo **variables** (build-time baked into the SPA bundle):
9292
The Mongo cluster is the source of truth for AgentOS. **Only the `agentos-server` connects to Mongo.** As of the post-0.2.1 dev build, the Python SDK no longer writes Mongo directly — it POSTs telemetry to the server's ingest endpoint (`AgentOSHttpSink``POST /agentos/api/ingest/events`), and the server owns all writes. (The old `AgentRegistrySink` + `MongoMessageSink` and the `motor` dep were removed — see the Python SDK history below.)
9393

9494
**Collections (database = the server's `MONGO_DATABASE`):**
95-
- `agent_registry` — one doc per registered agent (the server writes `source.type="library"` for harness-mode agents → AgentOS UI hides the chat-sandbox button for those, see commit `8d829b8`)
95+
- `agent_registry` — one doc per registered agent (the server writes `source.type="library"` for harness-mode agents → AgentOS UI hides the chat-sandbox button for those, see commit `8d829b8`). Also carries **ownership** (`ownerGroup`/`ownerUser`, see §2.6b) and **GAP source sync** (`sourceSha`/`sourceSyncedAt` — the commit SHA the SDK last loaded; the `session_started` projection updates it and logs drift, see §2.6c).
9696
- `agent_logs` — one doc per conversation (one `ComputerAgent` instance = one log row, multi-turn collapses correctly since the 0.2.0 session-id refactor)
9797
- `sessions` — ordered chat transcript (one doc per session_id, entries appended in order; **`session_started` is the sole creator** of the doc, so a dropped/reordered start can't stub it)
9898
- `chat_sessions` — the session-index row (`{_id, agent, createdAt, lastMessageAt}`) the dashboard's session list + per-agent `sessionCount`/`lastActivity` read. The server projection writes this so library-mode sessions show up (the old Python sink omitted it).
9999
- `agent_messages` — per-event audit trail (every assistant_message / tool_use / tool_result lands here)
100100
- `slack_threads` — Slack-bot chat-channel state only; **not** written by the ingest projection (it was dead/legacy for library agents).
101+
- `roles` — the DB-backed RBAC map (`{_id: <Keycloak role name>, permissions[], builtin}`), editable in Settings→Roles; seeded with `agentos-admin`/`-editor`/`-viewer` (§2.6b).
102+
- `api_keys` — AgentOS-issued service keys (`cak_…`), stored hashed; each carries `roleIds` (capability) + `group` (tenancy). Validated by the harness via introspection; permissions resolve from the same `roles` map (§2.6b).
103+
- `git_credentials` — group-scoped git PATs (encrypted at rest), one per `(ownerGroup, host)`, used by the SDK to clone private GAP repos (§2.6c).
104+
105+
Resources stamped with `ownerGroup`/`ownerUser` are **hard-isolated**: a non-admin sees only their own or their group's; admins (`*`) see all (§2.6b).
101106

102107
**Credentials required:**
103108
- On the **SDK** side: `AGENTOS_INGEST_URL` (e.g. `https://<host>/agentos/api/ingest/events`) + optional `AGENTOS_INGEST_TOKEN` (sent as `Authorization: Bearer …`). No Mongo creds.
@@ -107,6 +112,45 @@ The Mongo cluster is the source of truth for AgentOS. **Only the `agentos-server
107112

108113
---
109114

115+
### 2.6b AgentOS authentication + RBAC (Okta → Keycloak → BFF)
116+
117+
> The shared-password gate is gone. AgentOS now does real SSO + DB-backed RBAC + group ownership. Code lives under `packages/agentos-server/src/auth/`.
118+
119+
**Authentication — Okta federated by Keycloak, BFF session.** The app speaks only OIDC to Keycloak (which brokers Okta). `agentos-server` is the confidential `agent-os-server-client`: it runs Authorization Code + PKCE server-side (`auth/oidc.ts`, `routes/auth.ts`), verifies tokens via JWKS (`jose`), and sets an **httpOnly `agentos_session` cookie** carrying a signed principal snapshot — no token ever reaches the browser. The SPA is SSO-only (`LoginPage`).
120+
121+
**Token refresh (reactive).** The session cookie tracks the (short) access-token expiry; the server also holds a rotating refresh token in `agentos_refresh`. On a `401`, the SPA silently `POST /auth/refresh` (single-flight) and replays; a dead refresh token → SSO sign-in. So you stay logged in while active and only re-auth after Keycloak's SSO idle/max timeout.
122+
123+
**Authorization — DB-backed roles.** Keycloak emits role *names* (`realm_access.roles`) + `groups`; AgentOS owns what each role *can do* via the `roles` collection (editable in Settings→Roles). `authenticate → resolvePermissions → authorize(perm)` gates every dashboard route. Permission catalog is code-defined (`auth/permissions.ts`).
124+
125+
**Three guards / trust boundaries** (`app.ts`): SERVICE `/agentos/api/ingest/*` (`requireIngestAuth`, fails open) + `/agentos/api/keys/*` (`requireIntrospectionAuth`, fails closed); DASHBOARD `/agentos/api/v1/*` (`authenticate`); OBS `/v1/*`. `cak_` API keys authenticate at the dashboard boundary too (→ service principal with `groups=[key.group]`).
126+
127+
**Groups = read-only from Keycloak Admin API** (Settings→Groups). If a user's token lacks the `groups` claim, the server backfills groups from the Admin API at login/refresh (`auth/keycloak-admin.ts:listUserGroups`).
128+
129+
**Required env (server):**
130+
- `KEYCLOAK_ISSUER_URL` = `https://<kc-host>/realms/<realm>` (e.g. realm `computer-agent`)
131+
- `OIDC_CLIENT_ID` + `OIDC_CLIENT_SECRET` (confidential client); optional `OIDC_AUDIENCE`, `OIDC_REDIRECT_URI`, `OIDC_POST_LOGOUT_URI`, `OIDC_ROLES_CLAIM`/`OIDC_GROUPS_CLAIM`
132+
- `AGENTOS_SESSION_SECRET` (HMAC for the signed cookies — **stable in prod**)
133+
- `AGENTOS_DEFAULT_ROLE` (e.g. `agentos-viewer`) — fallback when the token has no AgentOS role
134+
- `AGENTOS_BOOTSTRAP_ADMINS` (comma-sep emails granted `*` before role lookup — first-admin bring-up; remove after)
135+
- `AGENTOS_DEV_AUTH=1`**local only** dev bypass injecting an admin principal; never in deployed envs
136+
- `KEYCLOAK_ADMIN_CLIENT_ID`/`SECRET` (defaults to the OIDC client) — service account needs `view-realm`/`view-users` for the Groups view + group backfill
137+
138+
> **Provisioning:** `pnpm --filter @computeragent/agentos-server provision:keycloak` (`scripts/provision-keycloak.mjs`) idempotently creates the realm, the three realm roles, the OIDC client (+ secret), the **Group Membership** mapper, and the service-account roles. Run with `DRY_RUN=1` first. Needs a Keycloak master-admin user/pass (used once, never stored).
139+
140+
### 2.6c Git credentials (private GAP repos) + SHA sync
141+
142+
> So the SDK can clone **private** GAP repos. Code: `auth/.../crypto/secret-box.ts`, `stores/git-credential-store.ts`, `routes/git-credentials.ts`; SDK side in `computeragent-py` (`harness/git_credential_client.py`, `substrates/local.py`).
143+
144+
- **Store.** A PAT is owned by a **group** and scoped to one **host** — one per `(ownerGroup, host)` in `git_credentials`, **AES-256-GCM encrypted at rest**. Managed in Settings→Git Credentials (perms `git-credentials:read`/`:manage`). The secret is write-only (never returned).
145+
- **Resolve.** The SDK calls `POST /agentos/api/v1/git-credentials/resolve` with its `cak_` key; the server returns the decrypted PAT for the key's group + the repo host (strictly group-scoped, no admin bypass). The SDK injects it via `GIT_CONFIG_*`/`http.<host>.extraHeader` so the token never lands in `argv`/URL; SSH URLs pass through. Miss/401 → unauthenticated clone fallback (public repos unaffected).
146+
- **SHA sync.** After cloning, the SDK runs `git rev-parse HEAD` and reports it as `agent_sha` on `session_started`; the projection writes `sourceSha`/`sourceSyncedAt` on the registry doc and logs any change. (Reactive — recorded on each run; the SDK already re-clones fresh, so the running agent is never stale.)
147+
148+
**Required env:**
149+
- Server: `AGENTOS_CREDENTIALS_KEY` (base64 of 32 random bytes; **fail-closed** — credentials CRUD/resolve 503 without it). Optional `AGENTOS_CREDENTIALS_KEY_OLD` for rotation.
150+
- SDK: `AGENTOS_API_URL` (e.g. `https://<host>/agentos/api/v1`) + the same `cak_` key it already uses (`COMPUTERAGENT_HARNESS_TOKEN` / `AGENTOS_INGEST_TOKEN`). The key's role must include `git-credentials:read`.
151+
152+
---
153+
110154
### 2.4 OpenTelemetry / New Relic
111155

112156
Every harness run emits GenAI-semconv spans + metrics through `OtelSink`. With env vars set, the sink ships out of process; without them it falls back to the console exporter.
@@ -276,9 +320,21 @@ OPENAI_API_KEY=sk-...
276320
# On the SDK (library/worker) side:
277321
AGENTOS_INGEST_URL=https://<agentos-host>/agentos/api/ingest/events
278322
AGENTOS_INGEST_TOKEN=<shared-secret> # optional; must match the server's
323+
AGENTOS_API_URL=https://<agentos-host>/agentos/api/v1 # for private-GAP credential resolve (§2.6c)
324+
COMPUTERAGENT_HARNESS_TOKEN=cak_... # the AgentOS API key the SDK presents (role needs git-credentials:read)
279325
# On the agentos-server side (NOT the SDK):
280326
MONGO_URL=mongodb+srv://user:pass@cluster.mongodb.net
281327
MONGO_DATABASE=computeragent
328+
# AgentOS auth / RBAC (§2.6b) — SSO via Keycloak (Okta brokered), DB-backed roles:
329+
KEYCLOAK_ISSUER_URL=https://<kc-host>/realms/computer-agent
330+
OIDC_CLIENT_ID=agent-os-server-client
331+
OIDC_CLIENT_SECRET=<confidential-client-secret>
332+
AGENTOS_SESSION_SECRET=<stable-hmac-secret>
333+
AGENTOS_DEFAULT_ROLE=agentos-viewer
334+
# AGENTOS_BOOTSTRAP_ADMINS=you@org.com # first-admin bring-up; remove after
335+
# AGENTOS_DEV_AUTH=1 # LOCAL ONLY — admin bypass, never deployed
336+
# Git credentials at rest (§2.6c):
337+
AGENTOS_CREDENTIALS_KEY=<base64 of 32 random bytes>
282338

283339
# OTel → New Relic
284340
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net
@@ -359,9 +415,12 @@ pnpm build && pnpm start # node dist/index.js
359415
| `CORS_ORIGIN` | empty | Comma-separated origins allowed to call the API (set to your SPA origin) |
360416
| `NODE_ENV` || `production` enables secure cookies + tightens defaults |
361417
| `COOKIE_SECURE` | derived from `NODE_ENV` | Force `true` / `false` explicitly |
362-
| `AGENTOS_SESSION_SECRET` | random per boot | Cookie-session secret. **Set to a stable value in prod** or sessions are invalidated on restart |
363-
| `API_AUTH_USER` + `API_AUTH_PASS` | unset | Basic-auth gate on the API. When unset the API is open (relies on network policy) |
418+
| `AGENTOS_SESSION_SECRET` | random per boot | HMAC secret for the signed BFF cookies (`agentos_session`/`agentos_refresh`). **Set to a stable value in prod** or every session is invalidated on restart |
419+
| `API_AUTH_USER` + `API_AUTH_PASS` | unset | **Legacy** — no longer gates the dashboard (SSO does, §2.6b). Now only used to build the Basic header for outbound loopback calls to the harness (`caAuthHeader`) |
364420
| `AGENTOS_INGEST_TOKEN` | unset | Bearer token guarding `POST /agentos/api/ingest/events` (the Python SDK's telemetry ingest). When unset the route is **open** (anonymous writes to registry/logs/sessions) — set it on any network-exposed pod. The SDK must send the same value as `AGENTOS_INGEST_TOKEN`. |
421+
| **Auth / RBAC** (§2.6b) || `KEYCLOAK_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` (+ optional `OIDC_AUDIENCE`/`OIDC_REDIRECT_URI`/`OIDC_POST_LOGOUT_URI`/`OIDC_ROLES_CLAIM`/`OIDC_GROUPS_CLAIM`); `AGENTOS_DEFAULT_ROLE`, `AGENTOS_BOOTSTRAP_ADMINS`, `AGENTOS_DEV_AUTH=1` (local only); `KEYCLOAK_ADMIN_CLIENT_ID`/`SECRET` for the Groups view + group backfill. Provision with `pnpm provision:keycloak`. |
422+
| **Git credentials** (§2.6c) | unset | `AGENTOS_CREDENTIALS_KEY` (base64 32B; **fail-closed** for credentials CRUD/resolve) + optional `AGENTOS_CREDENTIALS_KEY_OLD` for rotation. |
423+
| `AGENTOS_API_KEY_PEPPER` / `AGENTOS_INTROSPECTION_SECRET` | unset | HMAC pepper for `api_keys` hashing; shared secret guarding `/agentos/api/keys/introspect` (harness↔server) |
365424
| `AGENTOS_RUNTIME` | unset | Default substrate name used by the "Register agent" form (`local` / `bwrap` / `e2b` / `vzvm`) |
366425
| `AGENTOS_SEED_DEFAULT` | unset | Set to `1` to auto-seed a default agent into the registry on first boot |
367426
| `AGENTOS_DEFAULT_SOURCE` | `github.com/shreyas-lyzr/general-agent` | Used by the seed agent |
@@ -433,6 +492,10 @@ export ANTHROPIC_API_KEY=sk-ant-...
433492
export TRACE_BACKEND=newrelic
434493
export NEW_RELIC_USER_API_KEY=NRAK-...
435494
export NEW_RELIC_ACCOUNT_ID=1234567
495+
export AGENTOS_DEV_AUTH=1 # local only — admin principal, no Keycloak needed (§2.6b)
496+
# To exercise real SSO/RBAC locally instead, drop AGENTOS_DEV_AUTH and set the
497+
# KEYCLOAK_ISSUER_URL / OIDC_* vars (run `pnpm provision:keycloak` first).
498+
# For git-credentials locally: export AGENTOS_CREDENTIALS_KEY=$(openssl rand -base64 32)
436499
cd packages/agentos-server && pnpm dev
437500

438501
# Terminal 3 — SPA
@@ -470,6 +533,8 @@ Chronological from earliest to latest. Each entry has the commit ref where relev
470533
| `2756b9a` | `agentos-server`: dashboard API extracted into its own Express service; whole stack dockerized. |
471534
| `af47a08` | `engine-claude-agent-sdk`: set `IS_SANDBOX=1` for the spawned Claude CLI (skips first-run telemetry prompts and treats the host as a sandbox). |
472535
| `8d829b8` | `agentos`: introduced derived `liveChatCapable` field. SDK writes `source.type="library"` to `agent_registry` for harness-mode agents; UI checks `liveChatCapable` and hides the chat-sandbox button for those. Also strips model prefixes at every Mongo write site. |
536+
| `feat/agentos-auth-rbac-refresh` (pushed) | **AgentOS auth + RBAC overhaul (§2.6b).** Replaced the shared password with Okta→Keycloak OIDC (BFF, httpOnly cookie, `jose` JWKS), DB-backed roles (`roles` collection, Settings→Roles), `ownerGroup`/`ownerUser` hard isolation, reactive token refresh (`/auth/refresh`, rotating refresh cookie), read-only Groups view + Admin-API group backfill, `buildApp()` route restructure into versioned `/agentos/api/v1/*` + trust-boundary groups. SPA: AuthContext + `can()`-gated controls, SSO LoginPage, personalized-workspace home, refined agent cards (3/row). Added `scripts/provision-keycloak.mjs` (`pnpm provision:keycloak`). |
537+
| (same branch) | **Private-GAP git credentials + SHA sync (§2.6c).** `git_credentials` collection (AES-256-GCM at rest, one per `(ownerGroup,host)`), `POST /git-credentials/resolve` for the SDK's `cak_` key, `git-credentials:read`/`:manage` perms. SDK (`computeragent-py`): resolve client + `GIT_CONFIG_*` header injection (token never in argv) + `git rev-parse HEAD` capture → `agent_sha` → registry `sourceSha`/`sourceSyncedAt`. |
473538

474539
### Cross-cutting fixes worth knowing
475540

@@ -518,6 +583,13 @@ kustomize edit set image \
518583
| PyPI publish pipeline | `computer-agent-python-sdk/.github/workflows/publish.yml` |
519584
| SPA build args | `agentos/Dockerfile` + workflow `VITE_*` vars |
520585
| Mongo collections written by SDK | `computeragent-py/src/computeragent/telemetry/sinks/agentos.py` |
586+
| AgentOS auth / OIDC / BFF + refresh | `packages/agentos-server/src/auth/{oidc,authenticate,authorize,ownership,keycloak-admin}.ts`, `routes/auth.ts` |
587+
| Permission catalog + role seeds | `packages/agentos-server/src/auth/permissions.ts`, `stores/role-store.ts` |
588+
| Route composition / trust boundaries | `packages/agentos-server/src/app.ts`, `routes/dashboard.ts` |
589+
| Git-credential store + resolve endpoint | `packages/agentos-server/src/crypto/secret-box.ts`, `stores/git-credential-store.ts`, `routes/git-credentials.ts` |
590+
| SDK private-repo clone (PAT + SHA) | `computeragent-py/src/computeragent/harness/git_credential_client.py`, `substrates/local.py` |
591+
| Keycloak provisioning script | `packages/agentos-server/scripts/provision-keycloak.mjs` (`pnpm provision:keycloak`) |
521592
| Migration recipe (NordAssist QA) | `lyzr-experiments/NORDASSIST_MIGRATION.md` |
522593
| In-progress 0.2.1 plan | `~/.claude/plans/hey-i-need-the-idempotent-pretzel.md` |
594+
| GAP-auth + SHA-sync plan | `~/.claude/plans/reflective-mapping-lovelace.md` |
523595

agentos/src/api.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ export interface ApiKey {
249249
revokedAt?: string | null;
250250
}
251251

252+
/** A group-scoped git credential (PAT). The secret is never returned — only
253+
* metadata. One credential per (ownerGroup, host). */
254+
export interface GitCredential {
255+
_id: string;
256+
host: string;
257+
ownerGroup: string;
258+
ownerUser: string;
259+
label: string;
260+
username?: string | null;
261+
last4: string;
262+
hasSecret: true;
263+
createdBy: string;
264+
createdAt: string;
265+
updatedAt: string;
266+
rotatedAt?: string | null;
267+
}
268+
252269
// Current principal, from GET /me. Drives the SPA's permission gating.
253270
export interface Me {
254271
id: string; // principal id (Keycloak sub) — compare to resource ownerUser
@@ -558,6 +575,13 @@ export const api = {
558575
revoke: (id: string) => reqJSON<{ ok: boolean }>("DELETE", `/api-keys/${encodeURIComponent(id)}`),
559576
},
560577

578+
gitCredentials: {
579+
list: () => getJSON<{ credentials: GitCredential[] }>("/git-credentials").then((d) => d.credentials),
580+
create: (body: { host: string; group?: string; label: string; token: string; username?: string }) =>
581+
postJSON<{ credential: GitCredential }>("/git-credentials", body),
582+
remove: (id: string) => reqJSON<{ ok: boolean }>("DELETE", `/git-credentials/${encodeURIComponent(id)}`),
583+
},
584+
561585
// Evals — suite CRUD + run trigger + run readback.
562586
evals: {
563587
listSuites: () => getJSON<{ suites: EvalSuite[] }>("/evals/suites").then((d) => d.suites),

agentos/src/components/SettingsPage.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,41 @@
33
* Tabs are gated by the signed-in principal's permissions: "API Keys" needs
44
* keys:read, "Roles" needs roles:manage. A sign-out control lives at the bottom.
55
*/
6-
import { KeyRound, ShieldCheck, Users2, LogOut } from "lucide-react";
6+
import { KeyRound, ShieldCheck, Users2, LogOut, GitBranch } from "lucide-react";
77
import { PageHeader } from "./composite/PageHeader.tsx";
88
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs.tsx";
99
import { Button } from "./ui/button.tsx";
1010
import { ApiKeysSection } from "./settings/ApiKeysSection.tsx";
11+
import { GitCredentialsSection } from "./settings/GitCredentialsSection.tsx";
1112
import { RolesSection } from "./settings/RolesSection.tsx";
1213
import { GroupsSection } from "./settings/GroupsSection.tsx";
1314
import { useAuth } from "../context/AuthContext.tsx";
1415

1516
export function SettingsPage() {
1617
const { can, me, logout } = useAuth();
1718
const showKeys = can("keys:read");
19+
const showGitCreds = can("git-credentials:read");
1820
const showRoles = can("roles:manage");
1921
const showGroups = can("groups:read");
20-
const defaultTab = showKeys ? "api-keys" : showRoles ? "roles" : showGroups ? "groups" : "none";
22+
const defaultTab = showKeys ? "api-keys" : showGitCreds ? "git-credentials" : showRoles ? "roles" : showGroups ? "groups" : "none";
2123

2224
return (
2325
<div className="flex h-full flex-col">
2426
<PageHeader title="Settings" description="System-level configuration" />
2527
<div className="flex-1 overflow-y-auto px-6 py-5">
26-
{showKeys || showRoles || showGroups ? (
28+
{showKeys || showGitCreds || showRoles || showGroups ? (
2729
<Tabs defaultValue={defaultTab} className="w-full">
2830
<TabsList className="mb-5">
2931
{showKeys && (
3032
<TabsTrigger value="api-keys" className="gap-1.5">
3133
<KeyRound className="h-3.5 w-3.5" /> API Keys
3234
</TabsTrigger>
3335
)}
36+
{showGitCreds && (
37+
<TabsTrigger value="git-credentials" className="gap-1.5">
38+
<GitBranch className="h-3.5 w-3.5" /> Git Credentials
39+
</TabsTrigger>
40+
)}
3441
{showRoles && (
3542
<TabsTrigger value="roles" className="gap-1.5">
3643
<ShieldCheck className="h-3.5 w-3.5" /> Roles
@@ -47,6 +54,11 @@ export function SettingsPage() {
4754
<ApiKeysSection />
4855
</TabsContent>
4956
)}
57+
{showGitCreds && (
58+
<TabsContent value="git-credentials">
59+
<GitCredentialsSection />
60+
</TabsContent>
61+
)}
5062
{showRoles && (
5163
<TabsContent value="roles">
5264
<RolesSection />

0 commit comments

Comments
 (0)