agent-relay 1.0.22 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bridge/shadow-cli.d.ts +17 -0
- package/dist/bridge/shadow-cli.d.ts.map +1 -0
- package/dist/bridge/shadow-cli.js +75 -0
- package/dist/bridge/shadow-cli.js.map +1 -0
- package/dist/bridge/shadow-config.d.ts +87 -0
- package/dist/bridge/shadow-config.d.ts.map +1 -0
- package/dist/bridge/shadow-config.js +134 -0
- package/dist/bridge/shadow-config.js.map +1 -0
- package/dist/bridge/spawner.d.ts +68 -1
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +360 -16
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +67 -0
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +1196 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/api/auth.d.ts +20 -0
- package/dist/cloud/api/auth.d.ts.map +1 -0
- package/dist/cloud/api/auth.js +128 -0
- package/dist/cloud/api/auth.js.map +1 -0
- package/dist/cloud/api/billing.d.ts +17 -0
- package/dist/cloud/api/billing.d.ts.map +1 -0
- package/dist/cloud/api/billing.js +353 -0
- package/dist/cloud/api/billing.js.map +1 -0
- package/dist/cloud/api/cli-pty-runner.d.ts +54 -0
- package/dist/cloud/api/cli-pty-runner.d.ts.map +1 -0
- package/dist/cloud/api/cli-pty-runner.js +119 -0
- package/dist/cloud/api/cli-pty-runner.js.map +1 -0
- package/dist/cloud/api/coordinators.d.ts +8 -0
- package/dist/cloud/api/coordinators.d.ts.map +1 -0
- package/dist/cloud/api/coordinators.js +347 -0
- package/dist/cloud/api/coordinators.js.map +1 -0
- package/dist/cloud/api/daemons.d.ts +12 -0
- package/dist/cloud/api/daemons.d.ts.map +1 -0
- package/dist/cloud/api/daemons.js +320 -0
- package/dist/cloud/api/daemons.js.map +1 -0
- package/dist/cloud/api/generic-webhooks.d.ts +8 -0
- package/dist/cloud/api/generic-webhooks.d.ts.map +1 -0
- package/dist/cloud/api/generic-webhooks.js +129 -0
- package/dist/cloud/api/generic-webhooks.js.map +1 -0
- package/dist/cloud/api/git.d.ts +8 -0
- package/dist/cloud/api/git.d.ts.map +1 -0
- package/dist/cloud/api/git.js +131 -0
- package/dist/cloud/api/git.js.map +1 -0
- package/dist/cloud/api/github-app.d.ts +11 -0
- package/dist/cloud/api/github-app.d.ts.map +1 -0
- package/dist/cloud/api/github-app.js +189 -0
- package/dist/cloud/api/github-app.js.map +1 -0
- package/dist/cloud/api/middleware/planLimits.d.ts +43 -0
- package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
- package/dist/cloud/api/middleware/planLimits.js +202 -0
- package/dist/cloud/api/middleware/planLimits.js.map +1 -0
- package/dist/cloud/api/monitoring.d.ts +11 -0
- package/dist/cloud/api/monitoring.d.ts.map +1 -0
- package/dist/cloud/api/monitoring.js +578 -0
- package/dist/cloud/api/monitoring.js.map +1 -0
- package/dist/cloud/api/nango-auth.d.ts +9 -0
- package/dist/cloud/api/nango-auth.d.ts.map +1 -0
- package/dist/cloud/api/nango-auth.js +377 -0
- package/dist/cloud/api/nango-auth.js.map +1 -0
- package/dist/cloud/api/onboarding.d.ts +15 -0
- package/dist/cloud/api/onboarding.d.ts.map +1 -0
- package/dist/cloud/api/onboarding.js +588 -0
- package/dist/cloud/api/onboarding.js.map +1 -0
- package/dist/cloud/api/policy.d.ts +8 -0
- package/dist/cloud/api/policy.d.ts.map +1 -0
- package/dist/cloud/api/policy.js +229 -0
- package/dist/cloud/api/policy.js.map +1 -0
- package/dist/cloud/api/providers.d.ts +7 -0
- package/dist/cloud/api/providers.d.ts.map +1 -0
- package/dist/cloud/api/providers.js +507 -0
- package/dist/cloud/api/providers.js.map +1 -0
- package/dist/cloud/api/repos.d.ts +7 -0
- package/dist/cloud/api/repos.d.ts.map +1 -0
- package/dist/cloud/api/repos.js +314 -0
- package/dist/cloud/api/repos.js.map +1 -0
- package/dist/cloud/api/teams.d.ts +7 -0
- package/dist/cloud/api/teams.d.ts.map +1 -0
- package/dist/cloud/api/teams.js +279 -0
- package/dist/cloud/api/teams.js.map +1 -0
- package/dist/cloud/api/test-helpers.d.ts +10 -0
- package/dist/cloud/api/test-helpers.d.ts.map +1 -0
- package/dist/cloud/api/test-helpers.js +575 -0
- package/dist/cloud/api/test-helpers.js.map +1 -0
- package/dist/cloud/api/usage.d.ts +7 -0
- package/dist/cloud/api/usage.d.ts.map +1 -0
- package/dist/cloud/api/usage.js +98 -0
- package/dist/cloud/api/usage.js.map +1 -0
- package/dist/cloud/api/webhooks.d.ts +7 -0
- package/dist/cloud/api/webhooks.d.ts.map +1 -0
- package/dist/cloud/api/webhooks.js +496 -0
- package/dist/cloud/api/webhooks.js.map +1 -0
- package/dist/cloud/api/workspaces.d.ts +7 -0
- package/dist/cloud/api/workspaces.d.ts.map +1 -0
- package/dist/cloud/api/workspaces.js +727 -0
- package/dist/cloud/api/workspaces.js.map +1 -0
- package/dist/cloud/billing/index.d.ts +9 -0
- package/dist/cloud/billing/index.d.ts.map +1 -0
- package/dist/cloud/billing/index.js +9 -0
- package/dist/cloud/billing/index.js.map +1 -0
- package/dist/cloud/billing/plans.d.ts +39 -0
- package/dist/cloud/billing/plans.d.ts.map +1 -0
- package/dist/cloud/billing/plans.js +245 -0
- package/dist/cloud/billing/plans.js.map +1 -0
- package/dist/cloud/billing/service.d.ts +80 -0
- package/dist/cloud/billing/service.d.ts.map +1 -0
- package/dist/cloud/billing/service.js +388 -0
- package/dist/cloud/billing/service.js.map +1 -0
- package/dist/cloud/billing/types.d.ts +141 -0
- package/dist/cloud/billing/types.d.ts.map +1 -0
- package/dist/cloud/billing/types.js +7 -0
- package/dist/cloud/billing/types.js.map +1 -0
- package/dist/cloud/config.d.ts +66 -0
- package/dist/cloud/config.d.ts.map +1 -0
- package/dist/cloud/config.js +92 -0
- package/dist/cloud/config.js.map +1 -0
- package/dist/cloud/db/drizzle.d.ts +215 -0
- package/dist/cloud/db/drizzle.d.ts.map +1 -0
- package/dist/cloud/db/drizzle.js +1083 -0
- package/dist/cloud/db/drizzle.js.map +1 -0
- package/dist/cloud/db/index.d.ts +35 -0
- package/dist/cloud/db/index.d.ts.map +1 -0
- package/dist/cloud/db/index.js +52 -0
- package/dist/cloud/db/index.js.map +1 -0
- package/dist/cloud/db/schema.d.ts +4519 -0
- package/dist/cloud/db/schema.d.ts.map +1 -0
- package/dist/cloud/db/schema.js +547 -0
- package/dist/cloud/db/schema.js.map +1 -0
- package/dist/cloud/index.d.ts +12 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +39 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/provisioner/index.d.ts +75 -0
- package/dist/cloud/provisioner/index.d.ts.map +1 -0
- package/dist/cloud/provisioner/index.js +977 -0
- package/dist/cloud/provisioner/index.js.map +1 -0
- package/dist/cloud/server.d.ts +17 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +534 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/services/auto-scaler.d.ts +152 -0
- package/dist/cloud/services/auto-scaler.d.ts.map +1 -0
- package/dist/cloud/services/auto-scaler.js +439 -0
- package/dist/cloud/services/auto-scaler.js.map +1 -0
- package/dist/cloud/services/capacity-manager.d.ts +148 -0
- package/dist/cloud/services/capacity-manager.d.ts.map +1 -0
- package/dist/cloud/services/capacity-manager.js +449 -0
- package/dist/cloud/services/capacity-manager.js.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts +49 -0
- package/dist/cloud/services/ci-agent-spawner.d.ts.map +1 -0
- package/dist/cloud/services/ci-agent-spawner.js +373 -0
- package/dist/cloud/services/ci-agent-spawner.js.map +1 -0
- package/dist/cloud/services/coordinator.d.ts +62 -0
- package/dist/cloud/services/coordinator.d.ts.map +1 -0
- package/dist/cloud/services/coordinator.js +389 -0
- package/dist/cloud/services/coordinator.js.map +1 -0
- package/dist/cloud/services/index.d.ts +12 -0
- package/dist/cloud/services/index.d.ts.map +1 -0
- package/dist/cloud/services/index.js +15 -0
- package/dist/cloud/services/index.js.map +1 -0
- package/dist/cloud/services/mention-handler.d.ts +65 -0
- package/dist/cloud/services/mention-handler.d.ts.map +1 -0
- package/dist/cloud/services/mention-handler.js +405 -0
- package/dist/cloud/services/mention-handler.js.map +1 -0
- package/dist/cloud/services/nango.d.ts +126 -0
- package/dist/cloud/services/nango.d.ts.map +1 -0
- package/dist/cloud/services/nango.js +191 -0
- package/dist/cloud/services/nango.js.map +1 -0
- package/dist/cloud/services/persistence.d.ts +131 -0
- package/dist/cloud/services/persistence.d.ts.map +1 -0
- package/dist/cloud/services/persistence.js +200 -0
- package/dist/cloud/services/persistence.js.map +1 -0
- package/dist/cloud/services/planLimits.d.ts +125 -0
- package/dist/cloud/services/planLimits.d.ts.map +1 -0
- package/dist/cloud/services/planLimits.js +282 -0
- package/dist/cloud/services/planLimits.js.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts +159 -0
- package/dist/cloud/services/scaling-orchestrator.d.ts.map +1 -0
- package/dist/cloud/services/scaling-orchestrator.js +502 -0
- package/dist/cloud/services/scaling-orchestrator.js.map +1 -0
- package/dist/cloud/services/scaling-policy.d.ts +121 -0
- package/dist/cloud/services/scaling-policy.d.ts.map +1 -0
- package/dist/cloud/services/scaling-policy.js +415 -0
- package/dist/cloud/services/scaling-policy.js.map +1 -0
- package/dist/cloud/vault/index.d.ts +76 -0
- package/dist/cloud/vault/index.d.ts.map +1 -0
- package/dist/cloud/vault/index.js +219 -0
- package/dist/cloud/vault/index.js.map +1 -0
- package/dist/cloud/webhooks/index.d.ts +24 -0
- package/dist/cloud/webhooks/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/index.js +29 -0
- package/dist/cloud/webhooks/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/github.d.ts +8 -0
- package/dist/cloud/webhooks/parsers/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/github.js +234 -0
- package/dist/cloud/webhooks/parsers/github.js.map +1 -0
- package/dist/cloud/webhooks/parsers/index.d.ts +23 -0
- package/dist/cloud/webhooks/parsers/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/index.js +30 -0
- package/dist/cloud/webhooks/parsers/index.js.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/linear.js +258 -0
- package/dist/cloud/webhooks/parsers/linear.js.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts +9 -0
- package/dist/cloud/webhooks/parsers/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/parsers/slack.js +214 -0
- package/dist/cloud/webhooks/parsers/slack.js.map +1 -0
- package/dist/cloud/webhooks/responders/github.d.ts +8 -0
- package/dist/cloud/webhooks/responders/github.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/github.js +73 -0
- package/dist/cloud/webhooks/responders/github.js.map +1 -0
- package/dist/cloud/webhooks/responders/index.d.ts +23 -0
- package/dist/cloud/webhooks/responders/index.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/index.js +30 -0
- package/dist/cloud/webhooks/responders/index.js.map +1 -0
- package/dist/cloud/webhooks/responders/linear.d.ts +9 -0
- package/dist/cloud/webhooks/responders/linear.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/linear.js +149 -0
- package/dist/cloud/webhooks/responders/linear.js.map +1 -0
- package/dist/cloud/webhooks/responders/slack.d.ts +20 -0
- package/dist/cloud/webhooks/responders/slack.d.ts.map +1 -0
- package/dist/cloud/webhooks/responders/slack.js +178 -0
- package/dist/cloud/webhooks/responders/slack.js.map +1 -0
- package/dist/cloud/webhooks/router.d.ts +25 -0
- package/dist/cloud/webhooks/router.d.ts.map +1 -0
- package/dist/cloud/webhooks/router.js +504 -0
- package/dist/cloud/webhooks/router.js.map +1 -0
- package/dist/cloud/webhooks/rules-engine.d.ts +24 -0
- package/dist/cloud/webhooks/rules-engine.d.ts.map +1 -0
- package/dist/cloud/webhooks/rules-engine.js +287 -0
- package/dist/cloud/webhooks/rules-engine.js.map +1 -0
- package/dist/cloud/webhooks/types.d.ts +186 -0
- package/dist/cloud/webhooks/types.d.ts.map +1 -0
- package/dist/cloud/webhooks/types.js +8 -0
- package/dist/cloud/webhooks/types.js.map +1 -0
- package/dist/continuity/formatter.d.ts +51 -0
- package/dist/continuity/formatter.d.ts.map +1 -0
- package/dist/continuity/formatter.js +313 -0
- package/dist/continuity/formatter.js.map +1 -0
- package/dist/continuity/handoff-store.d.ts +67 -0
- package/dist/continuity/handoff-store.d.ts.map +1 -0
- package/dist/continuity/handoff-store.js +472 -0
- package/dist/continuity/handoff-store.js.map +1 -0
- package/dist/continuity/index.d.ts +45 -0
- package/dist/continuity/index.d.ts.map +1 -0
- package/dist/continuity/index.js +48 -0
- package/dist/continuity/index.js.map +1 -0
- package/dist/continuity/ledger-store.d.ts +110 -0
- package/dist/continuity/ledger-store.d.ts.map +1 -0
- package/dist/continuity/ledger-store.js +500 -0
- package/dist/continuity/ledger-store.js.map +1 -0
- package/dist/continuity/manager.d.ts +178 -0
- package/dist/continuity/manager.d.ts.map +1 -0
- package/dist/continuity/manager.js +562 -0
- package/dist/continuity/manager.js.map +1 -0
- package/dist/continuity/parser.d.ts +76 -0
- package/dist/continuity/parser.d.ts.map +1 -0
- package/dist/continuity/parser.js +579 -0
- package/dist/continuity/parser.js.map +1 -0
- package/dist/continuity/types.d.ts +180 -0
- package/dist/continuity/types.d.ts.map +1 -0
- package/dist/continuity/types.js +9 -0
- package/dist/continuity/types.js.map +1 -0
- package/dist/daemon/agent-manager.d.ts +114 -0
- package/dist/daemon/agent-manager.d.ts.map +1 -0
- package/dist/daemon/agent-manager.js +513 -0
- package/dist/daemon/agent-manager.js.map +1 -0
- package/dist/daemon/agent-registry.d.ts +34 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +45 -2
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/api.d.ts +81 -0
- package/dist/daemon/api.d.ts.map +1 -0
- package/dist/daemon/api.js +554 -0
- package/dist/daemon/api.js.map +1 -0
- package/dist/daemon/cli-auth.d.ts +67 -0
- package/dist/daemon/cli-auth.d.ts.map +1 -0
- package/dist/daemon/cli-auth.js +537 -0
- package/dist/daemon/cli-auth.js.map +1 -0
- package/dist/daemon/cloud-sync.d.ts +101 -0
- package/dist/daemon/cloud-sync.d.ts.map +1 -0
- package/dist/daemon/cloud-sync.js +263 -0
- package/dist/daemon/cloud-sync.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +6 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/orchestrator.d.ts +155 -0
- package/dist/daemon/orchestrator.d.ts.map +1 -0
- package/dist/daemon/orchestrator.js +766 -0
- package/dist/daemon/orchestrator.js.map +1 -0
- package/dist/daemon/router.d.ts +29 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +143 -21
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +42 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +199 -16
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/services/browser-testing.d.ts +88 -0
- package/dist/daemon/services/browser-testing.d.ts.map +1 -0
- package/dist/daemon/services/browser-testing.js +244 -0
- package/dist/daemon/services/browser-testing.js.map +1 -0
- package/dist/daemon/services/container-spawner.d.ts +135 -0
- package/dist/daemon/services/container-spawner.d.ts.map +1 -0
- package/dist/daemon/services/container-spawner.js +313 -0
- package/dist/daemon/services/container-spawner.js.map +1 -0
- package/dist/daemon/types.d.ts +131 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +6 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/workspace-manager.d.ts +75 -0
- package/dist/daemon/workspace-manager.d.ts.map +1 -0
- package/dist/daemon/workspace-manager.js +289 -0
- package/dist/daemon/workspace-manager.js.map +1 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/480-2d4111711d4e473c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/724-73c1ee5f60abe860.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-c3a14283c88d815b.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-7120be68bea622f3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-dc2e3a1a22478efc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/page-56a8b4616a90dc43.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-3eac37ea6f5dd153.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1081dd190a331a91.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-daf87e86f783f980.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-4d72d5a5d8a9b618.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-b68a681526eb145e.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-fee4ed1709070bcd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
- package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-97850e03d723ea8c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +1 -0
- package/dist/dashboard/out/_next/static/css/411ce23ffeae9f76.css +1 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
- package/dist/dashboard/out/alt-logos/logo.svg +38 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
- package/dist/dashboard/out/app.html +1 -0
- package/dist/dashboard/out/app.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -0
- package/dist/dashboard/out/connect-repos.txt +7 -0
- package/dist/dashboard/out/history.html +1 -0
- package/dist/dashboard/out/history.txt +7 -0
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +6 -0
- package/dist/dashboard/out/login.txt +7 -0
- package/dist/dashboard/out/metrics.html +1 -515
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +13 -0
- package/dist/dashboard/out/pricing.txt +7 -0
- package/dist/dashboard/out/providers.html +1 -0
- package/dist/dashboard/out/providers.txt +7 -0
- package/dist/dashboard/out/signup.html +6 -0
- package/dist/dashboard/out/signup.txt +7 -0
- package/dist/dashboard-server/metrics.d.ts.map +1 -1
- package/dist/dashboard-server/metrics.js +3 -2
- package/dist/dashboard-server/metrics.js.map +1 -1
- package/dist/dashboard-server/server.d.ts.map +1 -1
- package/dist/dashboard-server/server.js +2653 -130
- package/dist/dashboard-server/server.js.map +1 -1
- package/dist/hooks/emitter.d.ts +40 -0
- package/dist/hooks/emitter.d.ts.map +1 -0
- package/dist/hooks/emitter.js +63 -0
- package/dist/hooks/emitter.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/registry.d.ts +173 -0
- package/dist/hooks/registry.d.ts.map +1 -0
- package/dist/hooks/registry.js +476 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/trajectory-hooks.d.ts +52 -0
- package/dist/hooks/trajectory-hooks.d.ts.map +1 -0
- package/dist/hooks/trajectory-hooks.js +183 -0
- package/dist/hooks/trajectory-hooks.js.map +1 -0
- package/dist/hooks/types.d.ts +141 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/adapters/index.d.ts +8 -0
- package/dist/memory/adapters/index.d.ts.map +1 -0
- package/dist/memory/adapters/index.js +8 -0
- package/dist/memory/adapters/index.js.map +1 -0
- package/dist/memory/adapters/inmemory.d.ts +59 -0
- package/dist/memory/adapters/inmemory.d.ts.map +1 -0
- package/dist/memory/adapters/inmemory.js +195 -0
- package/dist/memory/adapters/inmemory.js.map +1 -0
- package/dist/memory/adapters/supermemory.d.ts +71 -0
- package/dist/memory/adapters/supermemory.d.ts.map +1 -0
- package/dist/memory/adapters/supermemory.js +338 -0
- package/dist/memory/adapters/supermemory.js.map +1 -0
- package/dist/memory/factory.d.ts +48 -0
- package/dist/memory/factory.d.ts.map +1 -0
- package/dist/memory/factory.js +143 -0
- package/dist/memory/factory.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +32 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memory-hooks.d.ts +60 -0
- package/dist/memory/memory-hooks.d.ts.map +1 -0
- package/dist/memory/memory-hooks.js +313 -0
- package/dist/memory/memory-hooks.js.map +1 -0
- package/dist/memory/service.d.ts +49 -0
- package/dist/memory/service.d.ts.map +1 -0
- package/dist/memory/service.js +146 -0
- package/dist/memory/service.js.map +1 -0
- package/dist/memory/types.d.ts +195 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +8 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/policy/agent-policy.d.ts +225 -0
- package/dist/policy/agent-policy.d.ts.map +1 -0
- package/dist/policy/agent-policy.js +665 -0
- package/dist/policy/agent-policy.js.map +1 -0
- package/dist/policy/cloud-policy-fetcher.d.ts +12 -0
- package/dist/policy/cloud-policy-fetcher.d.ts.map +1 -0
- package/dist/policy/cloud-policy-fetcher.js +64 -0
- package/dist/policy/cloud-policy-fetcher.js.map +1 -0
- package/dist/protocol/types.d.ts +10 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/resiliency/context-persistence.d.ts +140 -0
- package/dist/resiliency/context-persistence.d.ts.map +1 -0
- package/dist/resiliency/context-persistence.js +397 -0
- package/dist/resiliency/context-persistence.js.map +1 -0
- package/dist/resiliency/crash-insights.d.ts +156 -0
- package/dist/resiliency/crash-insights.d.ts.map +1 -0
- package/dist/resiliency/crash-insights.js +492 -0
- package/dist/resiliency/crash-insights.js.map +1 -0
- package/dist/resiliency/gossip-health.d.ts +137 -0
- package/dist/resiliency/gossip-health.d.ts.map +1 -0
- package/dist/resiliency/gossip-health.js +241 -0
- package/dist/resiliency/gossip-health.js.map +1 -0
- package/dist/resiliency/health-monitor.d.ts +97 -0
- package/dist/resiliency/health-monitor.d.ts.map +1 -0
- package/dist/resiliency/health-monitor.js +291 -0
- package/dist/resiliency/health-monitor.js.map +1 -0
- package/dist/resiliency/index.d.ts +68 -0
- package/dist/resiliency/index.d.ts.map +1 -0
- package/dist/resiliency/index.js +68 -0
- package/dist/resiliency/index.js.map +1 -0
- package/dist/resiliency/leader-watchdog.d.ts +109 -0
- package/dist/resiliency/leader-watchdog.d.ts.map +1 -0
- package/dist/resiliency/leader-watchdog.js +189 -0
- package/dist/resiliency/leader-watchdog.js.map +1 -0
- package/dist/resiliency/logger.d.ts +114 -0
- package/dist/resiliency/logger.d.ts.map +1 -0
- package/dist/resiliency/logger.js +250 -0
- package/dist/resiliency/logger.js.map +1 -0
- package/dist/resiliency/memory-monitor.d.ts +172 -0
- package/dist/resiliency/memory-monitor.d.ts.map +1 -0
- package/dist/resiliency/memory-monitor.js +593 -0
- package/dist/resiliency/memory-monitor.js.map +1 -0
- package/dist/resiliency/metrics.d.ts +115 -0
- package/dist/resiliency/metrics.d.ts.map +1 -0
- package/dist/resiliency/metrics.js +239 -0
- package/dist/resiliency/metrics.js.map +1 -0
- package/dist/resiliency/provider-context.d.ts +100 -0
- package/dist/resiliency/provider-context.d.ts.map +1 -0
- package/dist/resiliency/provider-context.js +360 -0
- package/dist/resiliency/provider-context.js.map +1 -0
- package/dist/resiliency/stateless-lead.d.ts +149 -0
- package/dist/resiliency/stateless-lead.d.ts.map +1 -0
- package/dist/resiliency/stateless-lead.js +308 -0
- package/dist/resiliency/stateless-lead.js.map +1 -0
- package/dist/resiliency/supervisor.d.ts +147 -0
- package/dist/resiliency/supervisor.d.ts.map +1 -0
- package/dist/resiliency/supervisor.js +459 -0
- package/dist/resiliency/supervisor.js.map +1 -0
- package/dist/shared/cli-auth-config.d.ts +91 -0
- package/dist/shared/cli-auth-config.d.ts.map +1 -0
- package/dist/shared/cli-auth-config.js +264 -0
- package/dist/shared/cli-auth-config.js.map +1 -0
- package/dist/storage/adapter.d.ts +3 -1
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +12 -2
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +18 -14
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/trajectory/config.d.ts +84 -0
- package/dist/trajectory/config.d.ts.map +1 -0
- package/dist/trajectory/config.js +163 -0
- package/dist/trajectory/config.js.map +1 -0
- package/dist/trajectory/index.d.ts +8 -0
- package/dist/trajectory/index.d.ts.map +1 -0
- package/dist/trajectory/index.js +8 -0
- package/dist/trajectory/index.js.map +1 -0
- package/dist/trajectory/integration.d.ts +292 -0
- package/dist/trajectory/integration.d.ts.map +1 -0
- package/dist/trajectory/integration.js +834 -0
- package/dist/trajectory/integration.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/project-namespace.d.ts +24 -0
- package/dist/utils/project-namespace.d.ts.map +1 -1
- package/dist/utils/project-namespace.js +84 -0
- package/dist/utils/project-namespace.js.map +1 -1
- package/dist/wrapper/client.d.ts +16 -1
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +32 -1
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +13 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +217 -47
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/pty-wrapper.d.ts +219 -17
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
- package/dist/wrapper/pty-wrapper.js +1050 -104
- package/dist/wrapper/pty-wrapper.js.map +1 -1
- package/dist/wrapper/shared.d.ts +165 -0
- package/dist/wrapper/shared.d.ts.map +1 -0
- package/dist/wrapper/shared.js +270 -0
- package/dist/wrapper/shared.js.map +1 -0
- package/dist/wrapper/tmux-wrapper.d.ts +78 -11
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +567 -106
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/CLOUD-ARCHITECTURE.md +804 -0
- package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
- package/docs/HOOKS_API.md +394 -0
- package/docs/WRAPPER_EVENTS.md +358 -0
- package/docs/agent-policy-snippet.md +40 -0
- package/docs/agent-relay-protocol.md +238 -0
- package/docs/agent-relay-snippet.md +115 -6
- package/docs/archive/EXECUTIVE_SUMMARY.md +358 -0
- package/docs/archive/ROADMAP.md +329 -0
- package/docs/archive/TESTING_PRESENCE_FEATURES.md +327 -0
- package/docs/competitive/GASTOWN.md +451 -0
- package/docs/{COMPETITIVE_ANALYSIS.md → competitive/OVERVIEW.md} +1 -0
- package/docs/competitive/README.md +34 -0
- package/docs/competitive/TMUX_ORCHESTRATOR.md +605 -0
- package/docs/dashboard.png +0 -0
- package/docs/design/ci-failure-webhooks.md +812 -0
- package/docs/design/comprehensive-integrations.md +238 -0
- package/docs/design/e2b-sandbox-integration.md +504 -0
- package/docs/design/github-app-permissions.md +264 -0
- package/docs/guides/CLOUD.md +236 -0
- package/docs/guides/LOCAL.md +535 -0
- package/docs/guides/SELF-HOSTED.md +494 -0
- package/docs/local-testing.md +428 -0
- package/docs/proposals/continuous-claude-integration.md +622 -0
- package/docs/proposals/custom-commands.md +368 -0
- package/docs/proposals/shadow-as-subagent.md +765 -0
- package/docs/proposals/slack-bot-integration.md +1457 -0
- package/docs/tasks/global-skills-system.tasks.md +230 -0
- package/docs/tasks/webhook-integrations.tasks.md +184 -0
- package/docs/tasks/workspace-capabilities.tasks.md +121 -0
- package/docs/testing/RESILIENCY-TEST-PLAN-2026-01-01.md +366 -0
- package/package.json +45 -7
- package/scripts/cloud-setup.sh +96 -0
- package/scripts/manual-qa.sh +293 -0
- package/scripts/postinstall.js +60 -0
- package/scripts/run-cloud-qa.sh +220 -0
- package/scripts/test-cli-auth/Dockerfile +44 -0
- package/scripts/test-cli-auth/Dockerfile.real +79 -0
- package/scripts/test-cli-auth/README.md +286 -0
- package/scripts/test-cli-auth/ci-test-real-clis.ts +251 -0
- package/scripts/test-cli-auth/ci-test-runner.ts +263 -0
- package/scripts/test-cli-auth/mock-cli.sh +147 -0
- package/scripts/test-cli-auth/package.json +14 -0
- package/scripts/test-cli-auth/test-oauth-flow.ts +220 -0
- package/scripts/test-pty-input-auto.js +222 -0
- package/scripts/test-pty-input.js +150 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-4498be09a5157759.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-c81f7fd28659d64f.js +0 -1
- package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
- /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{DXFA-jj8wb3PcY5DX2xcU → H5aWG0udPB4iOUIl_gytz}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b100311aff8d5c61.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-a13d3c2b1be45466.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-a4973f3e3c82fb67.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
- /package/docs/{CHANGELOG.md → archive/CHANGELOG.md} +0 -0
- /package/docs/{CLI-SIMPLIFICATION-COMPLETE.md → archive/CLI-SIMPLIFICATION-COMPLETE.md} +0 -0
- /package/docs/{DESIGN_BRIDGE_STAFFING.md → archive/DESIGN_BRIDGE_STAFFING.md} +0 -0
- /package/docs/{DESIGN_V2.md → archive/DESIGN_V2.md} +0 -0
- /package/docs/{MONETIZATION.md → archive/MONETIZATION.md} +0 -0
- /package/docs/{PROPOSAL-trajectories.md → archive/PROPOSAL-trajectories.md} +0 -0
- /package/docs/{SCALING_ANALYSIS.md → archive/SCALING_ANALYSIS.md} +0 -0
- /package/docs/{TMUX_IMPLEMENTATION_NOTES.md → archive/TMUX_IMPLEMENTATION_NOTES.md} +0 -0
- /package/docs/{TMUX_IMPROVEMENTS.md → archive/TMUX_IMPROVEMENTS.md} +0 -0
- /package/docs/{dashboard-v2-plan.md → archive/dashboard-v2-plan.md} +0 -0
- /package/docs/{removable-code-analysis.md → archive/removable-code-analysis.md} +0 -0
- /package/docs/{competitive-analysis-mcp-agent-mail.md → competitive/MCP_AGENT_MAIL.md} +0 -0
|
@@ -0,0 +1,1983 @@
|
|
|
1
|
+
# Agent Relay Cloud - Onboarding Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Agent Relay supports two deployment models. **Authentication is always handled by Agent Relay Cloud** - users don't self-host the auth system.
|
|
6
|
+
|
|
7
|
+
### Deployment Models
|
|
8
|
+
|
|
9
|
+
**Model 1: Cloud Hosted** - We run everything
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
13
|
+
│ AGENT RELAY CLOUD │
|
|
14
|
+
│ │
|
|
15
|
+
│ User connects accounts → We handle everything else │
|
|
16
|
+
│ │
|
|
17
|
+
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
18
|
+
│ │ Provider │ │ Agent │ │ Dashboard │ │
|
|
19
|
+
│ │ Auth │ │ Execution │ │ & Logs │ │
|
|
20
|
+
│ └────────────┘ └────────────┘ └────────────┘ │
|
|
21
|
+
│ ▲ │
|
|
22
|
+
│ │ │
|
|
23
|
+
│ ┌────────────┐ ┌─────┴──────┐ │
|
|
24
|
+
│ │ GitHub │──│ Cloned │ │
|
|
25
|
+
│ │ Webhooks │ │ Repos │ │
|
|
26
|
+
│ └────────────┘ └────────────┘ │
|
|
27
|
+
│ │
|
|
28
|
+
└────────────────────────────────────────────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Model 2: Self-Hosted** - User brings their own servers, auth via cloud
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ AGENT RELAY CLOUD │
|
|
36
|
+
│ │
|
|
37
|
+
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
38
|
+
│ │ Provider │ │ Dashboard │ │ Team │ │
|
|
39
|
+
│ │ Auth/Vault │ │ & Logs │ │ Management │ │
|
|
40
|
+
│ └─────┬──────┘ └─────▲──────┘ └────────────┘ │
|
|
41
|
+
└─────────┼───────────────┼──────────────────────────────────────┘
|
|
42
|
+
│ Credentials │ Sync
|
|
43
|
+
▼ │
|
|
44
|
+
┌─────────────────────────┴──────────────────────────────────────┐
|
|
45
|
+
│ USER'S INFRASTRUCTURE │
|
|
46
|
+
│ │
|
|
47
|
+
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
|
48
|
+
│ │agent-relay │ │ Agents │ │ Repos │ │
|
|
49
|
+
│ │ daemon │ │ (claude, │ │ (local) │ │
|
|
50
|
+
│ │ │ │ codex) │ │ │ │
|
|
51
|
+
│ └────────────┘ └────────────┘ └────────────┘ │
|
|
52
|
+
│ │
|
|
53
|
+
│ User's VMs, Kubernetes, containers, or bare metal │
|
|
54
|
+
└────────────────────────────────────────────────────────────────┘
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Feature Comparison
|
|
58
|
+
|
|
59
|
+
| Feature | Cloud Hosted | Self-Hosted |
|
|
60
|
+
|---------|--------------|-------------|
|
|
61
|
+
| **Provider auth** | ✅ Cloud (device flow) | ✅ Cloud (device flow) |
|
|
62
|
+
| **Credential vault** | ✅ Cloud | ✅ Cloud → synced to user's infra |
|
|
63
|
+
| **Dashboard** | ✅ Cloud | ✅ Cloud (synced from user's infra) |
|
|
64
|
+
| **Team features** | ✅ Full | ✅ Full |
|
|
65
|
+
| **Agent execution** | Our servers | User's servers |
|
|
66
|
+
| **Repos** | Cloned to cloud | User clones locally |
|
|
67
|
+
| **Compute costs** | Included in plan | User pays own infra |
|
|
68
|
+
| **Data locality** | Our cloud regions | User's choice |
|
|
69
|
+
|
|
70
|
+
### When to Use Each
|
|
71
|
+
|
|
72
|
+
**Cloud Hosted** - Best for:
|
|
73
|
+
- Teams wanting zero infrastructure management
|
|
74
|
+
- Quick start - no servers to provision
|
|
75
|
+
- Smaller teams without dedicated DevOps
|
|
76
|
+
- CI/CD integration with GitHub Actions
|
|
77
|
+
|
|
78
|
+
**Self-Hosted** - Best for:
|
|
79
|
+
- Enterprises requiring compute in specific regions/clouds
|
|
80
|
+
- Teams with existing Kubernetes/container infrastructure
|
|
81
|
+
- Cost optimization for high-volume usage
|
|
82
|
+
- Custom security requirements (VPC, firewall rules)
|
|
83
|
+
- GPU workloads on specialized hardware
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Cloud Hosted Mode
|
|
88
|
+
|
|
89
|
+
Agent Relay Cloud (fully hosted) provides:
|
|
90
|
+
- Automatic server provisioning with supervisor
|
|
91
|
+
- GitHub repository integration
|
|
92
|
+
- Multi-provider agent authentication via device flow
|
|
93
|
+
- Team management and collaboration
|
|
94
|
+
- Centralized dashboard and logs
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Self-Hosted Mode
|
|
99
|
+
|
|
100
|
+
For users running agent-relay on their own infrastructure. **Authentication happens via Agent Relay Cloud servers** - users connect to our infrastructure to run the browser-based auth flows.
|
|
101
|
+
|
|
102
|
+
### Why Self-Hosted Has More Friction (Intentional)
|
|
103
|
+
|
|
104
|
+
Self-hosted requires extra steps compared to cloud-hosted:
|
|
105
|
+
|
|
106
|
+
| Step | Cloud Hosted | Self-Hosted |
|
|
107
|
+
|------|--------------|-------------|
|
|
108
|
+
| 1. Sign up | GitHub OAuth | GitHub OAuth |
|
|
109
|
+
| 2. Connect providers | Click "Login with X" | Connect to cloud server, then "Login with X" |
|
|
110
|
+
| 3. Select repos | Select from list | Clone repos locally |
|
|
111
|
+
| 4. Start agents | Automatic | Install agent-relay, configure, start |
|
|
112
|
+
| 5. View dashboard | Just visit URL | Logs sync to cloud dashboard |
|
|
113
|
+
|
|
114
|
+
**This is intentional** - cloud-hosted should be the path of least resistance.
|
|
115
|
+
|
|
116
|
+
### Self-Hosted Auth Flow
|
|
117
|
+
|
|
118
|
+
Since Claude Code and Codex require browser-based OAuth, self-hosted users must connect to our cloud to authenticate:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
122
|
+
│ SELF-HOSTED AUTHENTICATION │
|
|
123
|
+
│ │
|
|
124
|
+
│ User's Server Agent Relay Cloud │
|
|
125
|
+
│ ───────────── ───────────────── │
|
|
126
|
+
│ │
|
|
127
|
+
│ 1. agent-relay cloud login │
|
|
128
|
+
│ │ │
|
|
129
|
+
│ │ ──────────────────────────────────────────────────> │
|
|
130
|
+
│ │ Connect to cloud auth service │
|
|
131
|
+
│ │ │
|
|
132
|
+
│ │ 2. Cloud opens browser │
|
|
133
|
+
│ │ to provider (Anthropic) │
|
|
134
|
+
│ │ │ │
|
|
135
|
+
│ │ ▼ │
|
|
136
|
+
│ │ 3. User logs in, │
|
|
137
|
+
│ │ authorizes │
|
|
138
|
+
│ │ │ │
|
|
139
|
+
│ │ ▼ │
|
|
140
|
+
│ │ 4. Tokens stored │
|
|
141
|
+
│ │ in cloud vault │
|
|
142
|
+
│ │ │
|
|
143
|
+
│ │ <────────────────────────────────────────────────── │
|
|
144
|
+
│ │ Sync encrypted credentials │
|
|
145
|
+
│ ▼ │
|
|
146
|
+
│ 5. Credentials cached locally │
|
|
147
|
+
│ (encrypted, auto-refresh via cloud) │
|
|
148
|
+
│ │
|
|
149
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Setup Flow
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# On user's server
|
|
156
|
+
npm install -g agent-relay
|
|
157
|
+
|
|
158
|
+
# Connect to cloud - opens browser on YOUR machine (not server)
|
|
159
|
+
# via a temporary secure tunnel
|
|
160
|
+
agent-relay cloud login
|
|
161
|
+
# → Opens: https://relay.cloud/auth/remote?session=abc123
|
|
162
|
+
# → You authenticate in your browser
|
|
163
|
+
# → Credentials sync to your server
|
|
164
|
+
|
|
165
|
+
# Connect providers (each opens browser via cloud)
|
|
166
|
+
agent-relay cloud connect anthropic
|
|
167
|
+
agent-relay cloud connect openai
|
|
168
|
+
|
|
169
|
+
# Start daemon - credentials synced from cloud
|
|
170
|
+
agent-relay up
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### CLI Experience
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
$ agent-relay cloud login
|
|
177
|
+
|
|
178
|
+
To authenticate, open this URL in your browser:
|
|
179
|
+
|
|
180
|
+
┌────────────────────────────────────────────────────────────┐
|
|
181
|
+
│ https://relay.cloud/auth/remote?session=xK9mPq2R │
|
|
182
|
+
└────────────────────────────────────────────────────────────┘
|
|
183
|
+
|
|
184
|
+
Or scan this QR code:
|
|
185
|
+
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
|
186
|
+
█ ▄▄▄▄▄ █ █ ▄▄█
|
|
187
|
+
█ █ █ █▄▄ ▀█
|
|
188
|
+
█ █▄▄▄█ █ ▄▀▀▄█
|
|
189
|
+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
|
190
|
+
|
|
191
|
+
⏳ Waiting for authentication...
|
|
192
|
+
Session expires in 9:45
|
|
193
|
+
|
|
194
|
+
✅ Authenticated as user@example.com
|
|
195
|
+
|
|
196
|
+
Connected providers:
|
|
197
|
+
• Anthropic (Claude) ❌ Not connected
|
|
198
|
+
• OpenAI (Codex) ❌ Not connected
|
|
199
|
+
|
|
200
|
+
To connect providers:
|
|
201
|
+
agent-relay cloud connect anthropic
|
|
202
|
+
|
|
203
|
+
$ agent-relay cloud connect anthropic
|
|
204
|
+
|
|
205
|
+
To connect Anthropic, open this URL in your browser:
|
|
206
|
+
|
|
207
|
+
┌────────────────────────────────────────────────────────────┐
|
|
208
|
+
│ https://relay.cloud/connect/anthropic?session=yL8nQr3S │
|
|
209
|
+
└────────────────────────────────────────────────────────────┘
|
|
210
|
+
|
|
211
|
+
⏳ Waiting for authorization...
|
|
212
|
+
|
|
213
|
+
✅ Anthropic connected!
|
|
214
|
+
Logged in as: claude-user@example.com
|
|
215
|
+
Plan: Claude Pro
|
|
216
|
+
|
|
217
|
+
Credentials synced to this server.
|
|
218
|
+
Run 'agent-relay up' to start.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Why This Approach
|
|
222
|
+
|
|
223
|
+
1. **Auth happens on cloud** - Browser-based OAuth works because it runs on our servers
|
|
224
|
+
2. **Credentials sync down** - Encrypted tokens pushed to user's server
|
|
225
|
+
3. **More steps than cloud** - Intentional friction encourages cloud adoption
|
|
226
|
+
4. **Still requires cloud** - Users can't fully disconnect from our service
|
|
227
|
+
5. **Refresh via cloud** - Token refresh happens through our service, maintaining dependency
|
|
228
|
+
|
|
229
|
+
### Credential Sync Architecture
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
233
|
+
│ AGENT RELAY CLOUD │
|
|
234
|
+
│ │
|
|
235
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
236
|
+
│ │ Credential Vault (encrypted) │ │
|
|
237
|
+
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
238
|
+
│ │ │Anthropic │ │ OpenAI │ │ Google │ │ │
|
|
239
|
+
│ │ │ tokens │ │ tokens │ │ tokens │ │ │
|
|
240
|
+
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
|
241
|
+
│ └───────┼─────────────┼─────────────┼─────────────────┘ │
|
|
242
|
+
│ └─────────────┼─────────────┘ │
|
|
243
|
+
│ │ │
|
|
244
|
+
│ ▼ Encrypted sync (TLS + E2E) │
|
|
245
|
+
└─────────────────────────┼───────────────────────────────────────┘
|
|
246
|
+
│
|
|
247
|
+
┌─────────────────────────▼───────────────────────────────────────┐
|
|
248
|
+
│ USER'S INFRASTRUCTURE │
|
|
249
|
+
│ │
|
|
250
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
251
|
+
│ │ agent-relay daemon │ │
|
|
252
|
+
│ │ │ │
|
|
253
|
+
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
|
254
|
+
│ │ │ Local credential cache (encrypted, auto-refresh) │ │ │
|
|
255
|
+
│ │ └────────────────────────────────────────────────────┘ │ │
|
|
256
|
+
│ │ │ │ │
|
|
257
|
+
│ │ ┌──────────┼──────────┐ │ │
|
|
258
|
+
│ │ ▼ ▼ ▼ │ │
|
|
259
|
+
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
|
260
|
+
│ │ │ claude │ │ codex │ │ gemini │ │ │
|
|
261
|
+
│ │ │ agent │ │ agent │ │ agent │ │ │
|
|
262
|
+
│ │ └────────┘ └────────┘ └────────┘ │ │
|
|
263
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
264
|
+
│ │
|
|
265
|
+
│ Logs synced back to cloud dashboard │
|
|
266
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Provider Authentication Architecture
|
|
272
|
+
|
|
273
|
+
### Design Principle: Login-Only Authentication
|
|
274
|
+
|
|
275
|
+
**No API keys in the UI** - Users authenticate via login flows, not by pasting keys. This provides:
|
|
276
|
+
|
|
277
|
+
- **Better security**: No keys to leak, rotate, or manage
|
|
278
|
+
- **Consistent UX**: Always "Login with X" buttons
|
|
279
|
+
- **Account linking**: Users authenticate with their existing provider accounts
|
|
280
|
+
- **Automatic token refresh**: Where supported
|
|
281
|
+
|
|
282
|
+
### Provider Authentication Reality Check
|
|
283
|
+
|
|
284
|
+
Different providers have different OAuth maturity levels. Notably, **both Claude Code and
|
|
285
|
+
OpenAI Codex** use browser-based OAuth that doesn't support headless/cloud environments well:
|
|
286
|
+
|
|
287
|
+
| Provider | Auth Flow | Status | Notes |
|
|
288
|
+
|----------|-----------|--------|-------|
|
|
289
|
+
| Claude/Anthropic | Browser OAuth | ⚠️ Device Flow | Opens browser, no redirect URI support |
|
|
290
|
+
| OpenAI/Codex | Browser OAuth | ⚠️ Device Flow | Opens browser for ChatGPT login |
|
|
291
|
+
| Google/Gemini | OAuth 2.0 | ✅ Redirect | Standard Google OAuth with redirect |
|
|
292
|
+
| GitHub Copilot | OAuth 2.0 | ✅ Redirect | Via GitHub OAuth (auto from signup) |
|
|
293
|
+
| Azure OpenAI | OAuth 2.0 | ✅ Redirect | Via Microsoft Entra ID |
|
|
294
|
+
| Local/Self-hosted | None | ✅ N/A | Just endpoint URL |
|
|
295
|
+
|
|
296
|
+
**Key insight**: The two most popular coding agents (Claude Code and Codex) both require
|
|
297
|
+
device flow or similar workarounds for cloud/headless environments. Both have open GitHub
|
|
298
|
+
issues requesting proper headless auth support:
|
|
299
|
+
- [anthropics/claude-code#7100](https://github.com/anthropics/claude-code/issues/7100)
|
|
300
|
+
- [openai/codex#2798](https://github.com/openai/codex/issues/2798)
|
|
301
|
+
|
|
302
|
+
### Device Flow Authentication Strategy (Claude + Codex)
|
|
303
|
+
|
|
304
|
+
Both Claude Code and OpenAI Codex use browser-based OAuth that stores tokens locally:
|
|
305
|
+
- **Claude Code**: `claude /login` → opens browser → stores in `~/.claude/.credentials.json`
|
|
306
|
+
- **Codex**: `codex` → opens browser for ChatGPT → stores in `~/.codex/`
|
|
307
|
+
|
|
308
|
+
For a cloud environment, we need a **device authorization flow** (RFC 8628):
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
312
|
+
│ Agent Relay Cloud - Claude Code Auth Flow │
|
|
313
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
314
|
+
│ │
|
|
315
|
+
│ Step 1: User clicks "Login with Anthropic" in our dashboard │
|
|
316
|
+
│ ↓ │
|
|
317
|
+
│ Step 2: Opens popup/redirect to Anthropic's OAuth │
|
|
318
|
+
│ (same flow as `claude /login`) │
|
|
319
|
+
│ ↓ │
|
|
320
|
+
│ Step 3: User authenticates with Anthropic │
|
|
321
|
+
│ ↓ │
|
|
322
|
+
│ Step 4: Anthropic redirects back with auth token │
|
|
323
|
+
│ ↓ │
|
|
324
|
+
│ Step 5: We store encrypted token in credential vault │
|
|
325
|
+
│ ↓ │
|
|
326
|
+
│ Step 6: When spawning agents, inject token via: │
|
|
327
|
+
│ - ANTHROPIC_AUTH_TOKEN env var, or │
|
|
328
|
+
│ - Mount equivalent of ~/.claude/.credentials.json │
|
|
329
|
+
│ │
|
|
330
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Note**: This requires Anthropic to support redirect-based OAuth (vs device-only flow). If not available, fallback options:
|
|
334
|
+
|
|
335
|
+
1. **Device Authorization Flow**: Display code, user enters at anthropic.com
|
|
336
|
+
2. **Credential File Upload**: User runs `/login` locally, uploads credential file
|
|
337
|
+
3. **API Key (hidden)**: Accept API key but label it as "Access Token" for consistent UX
|
|
338
|
+
|
|
339
|
+
### Proposed Solution: Provider Credentials Vault
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
343
|
+
│ Agent Relay Cloud │
|
|
344
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
345
|
+
│ │
|
|
346
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
347
|
+
│ │ GitHub │ │ Provider │ │ Secrets │ │
|
|
348
|
+
│ │ OAuth │ │ Connector │ │ Vault │ │
|
|
349
|
+
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
|
350
|
+
│ │ │ │ │
|
|
351
|
+
│ ▼ ▼ ▼ │
|
|
352
|
+
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
353
|
+
│ │ Onboarding Flow │ │
|
|
354
|
+
│ │ 1. Sign up (GitHub OAuth) │ │
|
|
355
|
+
│ │ 2. Connect repositories │ │
|
|
356
|
+
│ │ 3. Add agent providers │ │
|
|
357
|
+
│ │ 4. Configure teams │ │
|
|
358
|
+
│ └─────────────────────────────────────────────────────────┘ │
|
|
359
|
+
│ │
|
|
360
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Onboarding Flow Design
|
|
366
|
+
|
|
367
|
+
### Step 1: Sign Up via GitHub OAuth
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
┌─────────────────────────────────────────────┐
|
|
371
|
+
│ Welcome to Agent Relay Cloud │
|
|
372
|
+
│ │
|
|
373
|
+
│ Orchestrate AI agents across your repos │
|
|
374
|
+
│ │
|
|
375
|
+
│ ┌─────────────────────────────────────┐ │
|
|
376
|
+
│ │ 🔗 Continue with GitHub │ │
|
|
377
|
+
│ └─────────────────────────────────────┘ │
|
|
378
|
+
│ │
|
|
379
|
+
│ By signing up, you agree to our Terms │
|
|
380
|
+
└─────────────────────────────────────────────┘
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Why GitHub first?**
|
|
384
|
+
- Natural auth for developers
|
|
385
|
+
- Immediate access to repo list
|
|
386
|
+
- Repository permissions already defined
|
|
387
|
+
- GitHub Apps for webhook integration
|
|
388
|
+
|
|
389
|
+
### Step 2: Connect Repositories
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
393
|
+
│ Select Repositories │
|
|
394
|
+
│ │
|
|
395
|
+
│ Which repositories should Agent Relay manage? │
|
|
396
|
+
│ │
|
|
397
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
398
|
+
│ │ 🔍 Search repositories... │ │
|
|
399
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
400
|
+
│ │
|
|
401
|
+
│ ☑️ acme/frontend ⭐ 234 Updated 2 hours ago │
|
|
402
|
+
│ ☑️ acme/backend-api ⭐ 156 Updated 1 day ago │
|
|
403
|
+
│ ☐ acme/docs ⭐ 45 Updated 3 days ago │
|
|
404
|
+
│ ☐ acme/mobile-app ⭐ 89 Updated 1 week ago │
|
|
405
|
+
│ │
|
|
406
|
+
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
|
|
407
|
+
│ │ Back │ │ Continue with 2 repositories → │ │
|
|
408
|
+
│ └──────────────┘ └──────────────────────────────────────┘ │
|
|
409
|
+
└─────────────────────────────────────────────────────────────┘
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**What happens behind the scenes:**
|
|
413
|
+
- Install GitHub App on selected repos
|
|
414
|
+
- Clone repos to cloud workspace
|
|
415
|
+
- Detect existing `.claude/agents/` or `teams.json` configs
|
|
416
|
+
- Set up webhooks for PR/issue events
|
|
417
|
+
|
|
418
|
+
### Step 3: Add Agent Providers (The Key Step)
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
422
|
+
│ Connect Your AI Providers │
|
|
423
|
+
│ │
|
|
424
|
+
│ Agent Relay works with multiple AI providers. Connect the │
|
|
425
|
+
│ ones you want to use: │
|
|
426
|
+
│ │
|
|
427
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
428
|
+
│ │ ANTHROPIC ││
|
|
429
|
+
│ │ Claude Code ││
|
|
430
|
+
│ │ ⚡ Recommended for code tasks ││
|
|
431
|
+
│ │ ││
|
|
432
|
+
│ │ ┌─────────────────────────────────────────────────────┐ ││
|
|
433
|
+
│ │ │ 🔐 Login with Anthropic │ ││
|
|
434
|
+
│ │ └─────────────────────────────────────────────────────┘ ││
|
|
435
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
436
|
+
│ │
|
|
437
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
438
|
+
│ │ OPENAI ││
|
|
439
|
+
│ │ Codex, ChatGPT ││
|
|
440
|
+
│ │ Good for diverse tasks ││
|
|
441
|
+
│ │ ││
|
|
442
|
+
│ │ ┌─────────────────────────────────────────────────────┐ ││
|
|
443
|
+
│ │ │ 🔐 Login with OpenAI │ ││
|
|
444
|
+
│ │ └─────────────────────────────────────────────────────┘ ││
|
|
445
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
446
|
+
│ │
|
|
447
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
448
|
+
│ │ GOOGLE ││
|
|
449
|
+
│ │ Gemini ││
|
|
450
|
+
│ │ Multi-modal capabilities ││
|
|
451
|
+
│ │ ││
|
|
452
|
+
│ │ ┌─────────────────────────────────────────────────────┐ ││
|
|
453
|
+
│ │ │ 🔐 Login with Google │ ││
|
|
454
|
+
│ │ └─────────────────────────────────────────────────────┘ ││
|
|
455
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
456
|
+
│ │
|
|
457
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
458
|
+
│ │ GITHUB COPILOT ✓ Connected ││
|
|
459
|
+
│ │ Already connected via your GitHub account ││
|
|
460
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
461
|
+
│ │
|
|
462
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
463
|
+
│ │ + Add Self-Hosted Provider ││
|
|
464
|
+
│ │ Ollama, LM Studio, or other local tools ││
|
|
465
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
466
|
+
│ │
|
|
467
|
+
│ You can always add more providers later in Settings │
|
|
468
|
+
│ │
|
|
469
|
+
│ ┌──────────────┐ ┌──────────────────────────────────────────┐ │
|
|
470
|
+
│ │ Skip │ │ Continue → │ │
|
|
471
|
+
│ └──────────────┘ └──────────────────────────────────────────┘ │
|
|
472
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### OAuth Login Flow
|
|
476
|
+
|
|
477
|
+
When user clicks "Login with [Provider]":
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
481
|
+
│ │
|
|
482
|
+
│ ┌─────────────────────────────────────────┐ │
|
|
483
|
+
│ │ provider-name.com │ │
|
|
484
|
+
│ ├─────────────────────────────────────────┤ │
|
|
485
|
+
│ │ │ │
|
|
486
|
+
│ │ Sign in to Anthropic │ │
|
|
487
|
+
│ │ │ │
|
|
488
|
+
│ │ ┌───────────────────────────────────┐ │ │
|
|
489
|
+
│ │ │ email@example.com │ │ │
|
|
490
|
+
│ │ └───────────────────────────────────┘ │ │
|
|
491
|
+
│ │ ┌───────────────────────────────────┐ │ │
|
|
492
|
+
│ │ │ •••••••••••• │ │ │
|
|
493
|
+
│ │ └───────────────────────────────────┘ │ │
|
|
494
|
+
│ │ │ │
|
|
495
|
+
│ │ ┌───────────────────────────────────┐ │ │
|
|
496
|
+
│ │ │ Sign In │ │ │
|
|
497
|
+
│ │ └───────────────────────────────────┘ │ │
|
|
498
|
+
│ │ │ │
|
|
499
|
+
│ │ Or continue with: │ │
|
|
500
|
+
│ │ [Google] [GitHub] [SSO] │ │
|
|
501
|
+
│ │ │ │
|
|
502
|
+
│ └─────────────────────────────────────────┘ │
|
|
503
|
+
│ │
|
|
504
|
+
└─────────────────────────────────────────────────────────────┘
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
After login, authorization consent:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
511
|
+
│ │
|
|
512
|
+
│ ┌─────────────────────────────────────────┐ │
|
|
513
|
+
│ │ │ │
|
|
514
|
+
│ │ Authorize Agent Relay Cloud │ │
|
|
515
|
+
│ │ │ │
|
|
516
|
+
│ │ Agent Relay Cloud wants to: │ │
|
|
517
|
+
│ │ │ │
|
|
518
|
+
│ │ ✓ Run AI agents on your behalf │ │
|
|
519
|
+
│ │ ✓ Access your usage quota │ │
|
|
520
|
+
│ │ ✓ View your account info │ │
|
|
521
|
+
│ │ │ │
|
|
522
|
+
│ │ Signed in as: user@example.com │ │
|
|
523
|
+
│ │ │ │
|
|
524
|
+
│ │ ┌─────────────┐ ┌─────────────────┐ │ │
|
|
525
|
+
│ │ │ Cancel │ │ Authorize │ │ │
|
|
526
|
+
│ │ └─────────────┘ └─────────────────┘ │ │
|
|
527
|
+
│ │ │ │
|
|
528
|
+
│ └─────────────────────────────────────────┘ │
|
|
529
|
+
│ │
|
|
530
|
+
└─────────────────────────────────────────────────────────────┘
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
After authorization, redirect back with success:
|
|
534
|
+
|
|
535
|
+
```
|
|
536
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
537
|
+
│ Connect Your AI Providers │
|
|
538
|
+
│ │
|
|
539
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
540
|
+
│ │ ANTHROPIC ✓ Connected ││
|
|
541
|
+
│ │ Claude Code ││
|
|
542
|
+
│ │ Logged in as claude-user@example.com ││
|
|
543
|
+
│ │ [Disconnect] ││
|
|
544
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
545
|
+
│ ... │
|
|
546
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Device Authorization Flow (RFC 8628)
|
|
552
|
+
|
|
553
|
+
This is the **primary authentication method** for Claude Code and OpenAI Codex, since neither
|
|
554
|
+
supports standard OAuth redirect flows for third-party cloud applications.
|
|
555
|
+
|
|
556
|
+
### How Device Flow Works
|
|
557
|
+
|
|
558
|
+
```
|
|
559
|
+
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
|
560
|
+
│ Agent Relay │ │ Provider │ │ User's │
|
|
561
|
+
│ Cloud │ │ Auth Server │ │ Browser │
|
|
562
|
+
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
|
|
563
|
+
│ │ │
|
|
564
|
+
│ 1. POST /device/code │ │
|
|
565
|
+
│ {client_id, scope} │ │
|
|
566
|
+
│ ───────────────────────>│ │
|
|
567
|
+
│ │ │
|
|
568
|
+
│ {device_code, │ │
|
|
569
|
+
│ user_code: "ABCD-1234"│ │
|
|
570
|
+
│ verification_uri, │ │
|
|
571
|
+
│ expires_in: 900} │ │
|
|
572
|
+
│ <───────────────────────│ │
|
|
573
|
+
│ │ │
|
|
574
|
+
│ Display code to user │ │
|
|
575
|
+
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│
|
|
576
|
+
│ │ │
|
|
577
|
+
│ │ 2. User visits URL │
|
|
578
|
+
│ │ & enters user_code │
|
|
579
|
+
│ │<────────────────────────│
|
|
580
|
+
│ │ │
|
|
581
|
+
│ │ 3. User authenticates │
|
|
582
|
+
│ │ & authorizes app │
|
|
583
|
+
│ │<────────────────────────│
|
|
584
|
+
│ │ │
|
|
585
|
+
│ 4. POST /token │ │
|
|
586
|
+
│ {device_code} │ │
|
|
587
|
+
│ (polling every 5s) │ │
|
|
588
|
+
│ ───────────────────────>│ │
|
|
589
|
+
│ │ │
|
|
590
|
+
│ "authorization_pending"│ │
|
|
591
|
+
│ <───────────────────────│ (keep polling...) │
|
|
592
|
+
│ │ │
|
|
593
|
+
│ 5. POST /token │ │
|
|
594
|
+
│ ───────────────────────>│ │
|
|
595
|
+
│ │ │
|
|
596
|
+
│ {access_token, │ │
|
|
597
|
+
│ refresh_token} │ │
|
|
598
|
+
│ <───────────────────────│ │
|
|
599
|
+
│ │ │
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Provider-Specific Device Flow URLs
|
|
603
|
+
|
|
604
|
+
| Provider | Device Code URL | Token URL | Verification URL |
|
|
605
|
+
|----------|-----------------|-----------|------------------|
|
|
606
|
+
| Anthropic | `api.anthropic.com/oauth/device/code` | `api.anthropic.com/oauth/token` | `console.anthropic.com/device` |
|
|
607
|
+
| OpenAI | `auth.openai.com/device/code` | `auth.openai.com/oauth/token` | `auth.openai.com/device` |
|
|
608
|
+
| Google | `oauth2.googleapis.com/device/code` | `oauth2.googleapis.com/token` | `google.com/device` |
|
|
609
|
+
| GitHub | `github.com/login/device/code` | `github.com/login/oauth/access_token` | `github.com/login/device` |
|
|
610
|
+
|
|
611
|
+
*Note: Anthropic and OpenAI URLs are hypothetical - these providers would need to implement
|
|
612
|
+
RFC 8628 device authorization. Currently, they only support browser-based OAuth.*
|
|
613
|
+
|
|
614
|
+
### UI Flow
|
|
615
|
+
|
|
616
|
+
**Step 1: User clicks "Login with [Provider]"**
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
620
|
+
│ Connect Claude Code │
|
|
621
|
+
│ │
|
|
622
|
+
│ To connect your Anthropic account: │
|
|
623
|
+
│ │
|
|
624
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
625
|
+
│ │ 1. Click below to open Anthropic in a new tab ││
|
|
626
|
+
│ │ 2. Sign in with your Anthropic account ││
|
|
627
|
+
│ │ 3. Enter the code shown here when prompted ││
|
|
628
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
629
|
+
│ │
|
|
630
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
631
|
+
│ │ 🔐 Open Anthropic → ││
|
|
632
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
633
|
+
│ │
|
|
634
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Step 2: Show code, user enters at provider**
|
|
638
|
+
|
|
639
|
+
```
|
|
640
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
641
|
+
│ Connect Claude Code │
|
|
642
|
+
│ │
|
|
643
|
+
│ Enter this code at Anthropic: │
|
|
644
|
+
│ │
|
|
645
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
646
|
+
│ │ ││
|
|
647
|
+
│ │ WDJB-MJPV ││
|
|
648
|
+
│ │ ││
|
|
649
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
650
|
+
│ [Copy] │
|
|
651
|
+
│ │
|
|
652
|
+
│ A browser tab should have opened to console.anthropic.com │
|
|
653
|
+
│ Didn't open? Click here → │
|
|
654
|
+
│ │
|
|
655
|
+
│ ───────────────────────────────────────────────────────────────│
|
|
656
|
+
│ │
|
|
657
|
+
│ ⏳ Waiting for you to authorize... │
|
|
658
|
+
│ Code expires in 14:32 │
|
|
659
|
+
│ │
|
|
660
|
+
│ ┌──────────────┐ │
|
|
661
|
+
│ │ Cancel │ │
|
|
662
|
+
│ └──────────────┘ │
|
|
663
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Same flow for Codex:**
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
670
|
+
│ Connect OpenAI Codex │
|
|
671
|
+
│ │
|
|
672
|
+
│ Enter this code at OpenAI: │
|
|
673
|
+
│ │
|
|
674
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
675
|
+
│ │ ││
|
|
676
|
+
│ │ XKCD-4815 ││
|
|
677
|
+
│ │ ││
|
|
678
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
679
|
+
│ [Copy] │
|
|
680
|
+
│ │
|
|
681
|
+
│ A browser tab should have opened to auth.openai.com │
|
|
682
|
+
│ Didn't open? Click here → │
|
|
683
|
+
│ │
|
|
684
|
+
│ ───────────────────────────────────────────────────────────────│
|
|
685
|
+
│ │
|
|
686
|
+
│ ⏳ Waiting for you to authorize... │
|
|
687
|
+
│ Code expires in 14:47 │
|
|
688
|
+
│ │
|
|
689
|
+
│ ┌──────────────┐ │
|
|
690
|
+
│ │ Cancel │ │
|
|
691
|
+
│ └──────────────┘ │
|
|
692
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Step 3: Success**
|
|
696
|
+
|
|
697
|
+
```
|
|
698
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
699
|
+
│ │
|
|
700
|
+
│ ✅ Connected! │
|
|
701
|
+
│ │
|
|
702
|
+
│ Your Anthropic account is now linked. │
|
|
703
|
+
│ │
|
|
704
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
705
|
+
│ │ Account: user@example.com ││
|
|
706
|
+
│ │ Plan: Claude Pro ││
|
|
707
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
708
|
+
│ │
|
|
709
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
710
|
+
│ │ Continue → ││
|
|
711
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
712
|
+
│ │
|
|
713
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
**Error States:**
|
|
717
|
+
|
|
718
|
+
```
|
|
719
|
+
Code Expired:
|
|
720
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
721
|
+
│ ⚠️ Code Expired │
|
|
722
|
+
│ │
|
|
723
|
+
│ The authorization code has expired. This happens if you │
|
|
724
|
+
│ don't complete the sign-in within 15 minutes. │
|
|
725
|
+
│ │
|
|
726
|
+
│ ┌───────────────────┐ ┌───────────────────────────────────┐ │
|
|
727
|
+
│ │ Cancel │ │ Get New Code │ │
|
|
728
|
+
│ └───────────────────┘ └───────────────────────────────────┘ │
|
|
729
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
730
|
+
|
|
731
|
+
Access Denied:
|
|
732
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
733
|
+
│ ❌ Access Denied │
|
|
734
|
+
│ │
|
|
735
|
+
│ You denied the authorization request at Anthropic. │
|
|
736
|
+
│ │
|
|
737
|
+
│ ┌───────────────────┐ ┌───────────────────────────────────┐ │
|
|
738
|
+
│ │ Cancel │ │ Try Again │ │
|
|
739
|
+
│ └───────────────────┘ └───────────────────────────────────┘ │
|
|
740
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
### Device Flow Implementation
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
// src/cloud/auth/device-flow.ts
|
|
749
|
+
|
|
750
|
+
interface DeviceCodeResponse {
|
|
751
|
+
device_code: string; // Secret - we use this for polling
|
|
752
|
+
user_code: string; // Display to user: "WDJB-MJPV"
|
|
753
|
+
verification_uri: string; // Where user goes: console.anthropic.com/device
|
|
754
|
+
verification_uri_complete?: string; // URL with code pre-filled (optional)
|
|
755
|
+
expires_in: number; // Seconds until codes expire (typically 900)
|
|
756
|
+
interval: number; // Min seconds between poll requests (typically 5)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
interface DeviceFlowConfig {
|
|
760
|
+
provider: string;
|
|
761
|
+
deviceCodeUrl: string;
|
|
762
|
+
tokenUrl: string;
|
|
763
|
+
clientId: string;
|
|
764
|
+
scopes: string[];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const DEVICE_FLOW_CONFIGS: Record<string, DeviceFlowConfig> = {
|
|
768
|
+
anthropic: {
|
|
769
|
+
provider: 'anthropic',
|
|
770
|
+
deviceCodeUrl: 'https://api.anthropic.com/oauth/device/code',
|
|
771
|
+
tokenUrl: 'https://api.anthropic.com/oauth/token',
|
|
772
|
+
clientId: process.env.ANTHROPIC_CLIENT_ID!,
|
|
773
|
+
scopes: ['claude-code:execute', 'user:read']
|
|
774
|
+
},
|
|
775
|
+
openai: {
|
|
776
|
+
provider: 'openai',
|
|
777
|
+
deviceCodeUrl: 'https://auth.openai.com/device/code',
|
|
778
|
+
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
779
|
+
clientId: process.env.OPENAI_CLIENT_ID!,
|
|
780
|
+
scopes: ['openid', 'profile', 'email', 'codex:execute']
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
class DeviceFlowAuth {
|
|
785
|
+
private config: DeviceFlowConfig;
|
|
786
|
+
|
|
787
|
+
constructor(provider: string) {
|
|
788
|
+
this.config = DEVICE_FLOW_CONFIGS[provider];
|
|
789
|
+
if (!this.config) {
|
|
790
|
+
throw new Error(`No device flow config for provider: ${provider}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Step 1: Request device and user codes from provider
|
|
796
|
+
*/
|
|
797
|
+
async requestCodes(): Promise<DeviceCodeResponse> {
|
|
798
|
+
const response = await fetch(this.config.deviceCodeUrl, {
|
|
799
|
+
method: 'POST',
|
|
800
|
+
headers: {
|
|
801
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
802
|
+
},
|
|
803
|
+
body: new URLSearchParams({
|
|
804
|
+
client_id: this.config.clientId,
|
|
805
|
+
scope: this.config.scopes.join(' ')
|
|
806
|
+
})
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
if (!response.ok) {
|
|
810
|
+
const error = await response.text();
|
|
811
|
+
throw new Error(`Failed to get device code: ${error}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return response.json();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Step 2: Poll for tokens (called repeatedly until success/failure)
|
|
819
|
+
*/
|
|
820
|
+
async pollForToken(deviceCode: string): Promise<PollResult> {
|
|
821
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: {
|
|
824
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
825
|
+
},
|
|
826
|
+
body: new URLSearchParams({
|
|
827
|
+
client_id: this.config.clientId,
|
|
828
|
+
device_code: deviceCode,
|
|
829
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
830
|
+
})
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const data = await response.json();
|
|
834
|
+
|
|
835
|
+
// RFC 8628 error codes
|
|
836
|
+
if (data.error) {
|
|
837
|
+
switch (data.error) {
|
|
838
|
+
case 'authorization_pending':
|
|
839
|
+
// User hasn't completed authorization yet
|
|
840
|
+
return { status: 'pending' };
|
|
841
|
+
|
|
842
|
+
case 'slow_down':
|
|
843
|
+
// Polling too fast - increase interval
|
|
844
|
+
return {
|
|
845
|
+
status: 'slow_down',
|
|
846
|
+
retryAfter: data.interval || 10
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
case 'expired_token':
|
|
850
|
+
// Device code expired
|
|
851
|
+
return { status: 'expired' };
|
|
852
|
+
|
|
853
|
+
case 'access_denied':
|
|
854
|
+
// User denied authorization
|
|
855
|
+
return { status: 'denied' };
|
|
856
|
+
|
|
857
|
+
default:
|
|
858
|
+
return {
|
|
859
|
+
status: 'error',
|
|
860
|
+
error: data.error_description || data.error
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Success!
|
|
866
|
+
return {
|
|
867
|
+
status: 'success',
|
|
868
|
+
tokens: {
|
|
869
|
+
accessToken: data.access_token,
|
|
870
|
+
refreshToken: data.refresh_token,
|
|
871
|
+
expiresIn: data.expires_in,
|
|
872
|
+
scope: data.scope
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
type PollResult =
|
|
879
|
+
| { status: 'pending' }
|
|
880
|
+
| { status: 'slow_down'; retryAfter: number }
|
|
881
|
+
| { status: 'expired' }
|
|
882
|
+
| { status: 'denied' }
|
|
883
|
+
| { status: 'error'; error: string }
|
|
884
|
+
| { status: 'success'; tokens: TokenSet };
|
|
885
|
+
|
|
886
|
+
interface TokenSet {
|
|
887
|
+
accessToken: string;
|
|
888
|
+
refreshToken: string;
|
|
889
|
+
expiresIn: number;
|
|
890
|
+
scope: string;
|
|
891
|
+
}
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Device Flow API Routes
|
|
895
|
+
|
|
896
|
+
```typescript
|
|
897
|
+
// src/cloud/api/device-flow.ts
|
|
898
|
+
|
|
899
|
+
const router = Router();
|
|
900
|
+
|
|
901
|
+
// Store for active device flows (use Redis in production)
|
|
902
|
+
const activeFlows = new Map<string, ActiveFlow>();
|
|
903
|
+
|
|
904
|
+
interface ActiveFlow {
|
|
905
|
+
userId: string;
|
|
906
|
+
provider: string;
|
|
907
|
+
deviceCode: string;
|
|
908
|
+
userCode: string;
|
|
909
|
+
verificationUri: string;
|
|
910
|
+
verificationUriComplete?: string;
|
|
911
|
+
expiresAt: Date;
|
|
912
|
+
pollInterval: number;
|
|
913
|
+
status: 'pending' | 'success' | 'expired' | 'denied' | 'error';
|
|
914
|
+
tokens?: TokenSet;
|
|
915
|
+
error?: string;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* POST /api/device-flow/:provider/start
|
|
920
|
+
* Initiates device flow, returns user code to display
|
|
921
|
+
*/
|
|
922
|
+
router.post('/:provider/start', async (req, res) => {
|
|
923
|
+
const { provider } = req.params;
|
|
924
|
+
const userId = req.session.userId;
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const auth = new DeviceFlowAuth(provider);
|
|
928
|
+
const codes = await auth.requestCodes();
|
|
929
|
+
|
|
930
|
+
const flowId = crypto.randomUUID();
|
|
931
|
+
|
|
932
|
+
activeFlows.set(flowId, {
|
|
933
|
+
userId,
|
|
934
|
+
provider,
|
|
935
|
+
deviceCode: codes.device_code,
|
|
936
|
+
userCode: codes.user_code,
|
|
937
|
+
verificationUri: codes.verification_uri,
|
|
938
|
+
verificationUriComplete: codes.verification_uri_complete,
|
|
939
|
+
expiresAt: new Date(Date.now() + codes.expires_in * 1000),
|
|
940
|
+
pollInterval: codes.interval,
|
|
941
|
+
status: 'pending'
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// Start background polling
|
|
945
|
+
pollInBackground(flowId, auth);
|
|
946
|
+
|
|
947
|
+
res.json({
|
|
948
|
+
flowId,
|
|
949
|
+
userCode: codes.user_code,
|
|
950
|
+
verificationUri: codes.verification_uri,
|
|
951
|
+
verificationUriComplete: codes.verification_uri_complete,
|
|
952
|
+
expiresIn: codes.expires_in
|
|
953
|
+
});
|
|
954
|
+
} catch (error) {
|
|
955
|
+
res.status(500).json({ error: error.message });
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* GET /api/device-flow/:provider/status/:flowId
|
|
961
|
+
* Check status of device flow (client polls this)
|
|
962
|
+
*/
|
|
963
|
+
router.get('/:provider/status/:flowId', async (req, res) => {
|
|
964
|
+
const { flowId } = req.params;
|
|
965
|
+
const flow = activeFlows.get(flowId);
|
|
966
|
+
|
|
967
|
+
if (!flow) {
|
|
968
|
+
return res.status(404).json({ error: 'Flow not found' });
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (flow.userId !== req.session.userId) {
|
|
972
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const timeLeft = Math.max(0, Math.floor(
|
|
976
|
+
(flow.expiresAt.getTime() - Date.now()) / 1000
|
|
977
|
+
));
|
|
978
|
+
|
|
979
|
+
res.json({
|
|
980
|
+
status: flow.status,
|
|
981
|
+
expiresIn: timeLeft,
|
|
982
|
+
error: flow.error
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* DELETE /api/device-flow/:provider/:flowId
|
|
988
|
+
* Cancel device flow
|
|
989
|
+
*/
|
|
990
|
+
router.delete('/:provider/:flowId', async (req, res) => {
|
|
991
|
+
const { flowId } = req.params;
|
|
992
|
+
const flow = activeFlows.get(flowId);
|
|
993
|
+
|
|
994
|
+
if (flow?.userId === req.session.userId) {
|
|
995
|
+
activeFlows.delete(flowId);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
res.json({ success: true });
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Background polling for device authorization
|
|
1003
|
+
*/
|
|
1004
|
+
async function pollInBackground(flowId: string, auth: DeviceFlowAuth) {
|
|
1005
|
+
const flow = activeFlows.get(flowId);
|
|
1006
|
+
if (!flow) return;
|
|
1007
|
+
|
|
1008
|
+
let interval = flow.pollInterval * 1000;
|
|
1009
|
+
|
|
1010
|
+
const poll = async () => {
|
|
1011
|
+
const current = activeFlows.get(flowId);
|
|
1012
|
+
if (!current || current.status !== 'pending') return;
|
|
1013
|
+
|
|
1014
|
+
// Check expiry
|
|
1015
|
+
if (Date.now() > current.expiresAt.getTime()) {
|
|
1016
|
+
current.status = 'expired';
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
const result = await auth.pollForToken(current.deviceCode);
|
|
1022
|
+
|
|
1023
|
+
switch (result.status) {
|
|
1024
|
+
case 'pending':
|
|
1025
|
+
setTimeout(poll, interval);
|
|
1026
|
+
break;
|
|
1027
|
+
|
|
1028
|
+
case 'slow_down':
|
|
1029
|
+
interval = result.retryAfter * 1000;
|
|
1030
|
+
setTimeout(poll, interval);
|
|
1031
|
+
break;
|
|
1032
|
+
|
|
1033
|
+
case 'success':
|
|
1034
|
+
// Store tokens
|
|
1035
|
+
await storeProviderTokens(current.userId, current.provider, result.tokens);
|
|
1036
|
+
current.status = 'success';
|
|
1037
|
+
current.tokens = result.tokens;
|
|
1038
|
+
// Clean up after 60s
|
|
1039
|
+
setTimeout(() => activeFlows.delete(flowId), 60000);
|
|
1040
|
+
break;
|
|
1041
|
+
|
|
1042
|
+
case 'expired':
|
|
1043
|
+
case 'denied':
|
|
1044
|
+
current.status = result.status;
|
|
1045
|
+
break;
|
|
1046
|
+
|
|
1047
|
+
case 'error':
|
|
1048
|
+
current.status = 'error';
|
|
1049
|
+
current.error = result.error;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
console.error('Poll error:', error);
|
|
1054
|
+
// Retry with backoff
|
|
1055
|
+
setTimeout(poll, interval * 2);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// Start after initial interval
|
|
1060
|
+
setTimeout(poll, interval);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Store tokens after successful device flow
|
|
1065
|
+
*/
|
|
1066
|
+
async function storeProviderTokens(
|
|
1067
|
+
userId: string,
|
|
1068
|
+
provider: string,
|
|
1069
|
+
tokens: TokenSet
|
|
1070
|
+
) {
|
|
1071
|
+
// Get user info from provider
|
|
1072
|
+
const userInfo = await fetchProviderUserInfo(provider, tokens.accessToken);
|
|
1073
|
+
|
|
1074
|
+
await credentialVault.store({
|
|
1075
|
+
userId,
|
|
1076
|
+
provider,
|
|
1077
|
+
accessToken: tokens.accessToken,
|
|
1078
|
+
refreshToken: tokens.refreshToken,
|
|
1079
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),
|
|
1080
|
+
scopes: tokens.scope.split(' '),
|
|
1081
|
+
providerAccountId: userInfo.id,
|
|
1082
|
+
providerAccountEmail: userInfo.email,
|
|
1083
|
+
providerAccountName: userInfo.name,
|
|
1084
|
+
connectedAt: new Date(),
|
|
1085
|
+
isValid: true
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Frontend Component
|
|
1091
|
+
|
|
1092
|
+
```tsx
|
|
1093
|
+
// src/cloud/components/DeviceFlowAuth.tsx
|
|
1094
|
+
|
|
1095
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
1096
|
+
|
|
1097
|
+
interface Props {
|
|
1098
|
+
provider: 'anthropic' | 'openai';
|
|
1099
|
+
providerName: string; // "Anthropic" or "OpenAI"
|
|
1100
|
+
onSuccess: () => void;
|
|
1101
|
+
onCancel: () => void;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
type State =
|
|
1105
|
+
| { step: 'ready' }
|
|
1106
|
+
| { step: 'loading' }
|
|
1107
|
+
| { step: 'showing_code'; flowId: string; userCode: string;
|
|
1108
|
+
verificationUri: string; expiresAt: Date }
|
|
1109
|
+
| { step: 'success' }
|
|
1110
|
+
| { step: 'error'; message: string; canRetry: boolean };
|
|
1111
|
+
|
|
1112
|
+
export function DeviceFlowAuth({ provider, providerName, onSuccess, onCancel }: Props) {
|
|
1113
|
+
const [state, setState] = useState<State>({ step: 'ready' });
|
|
1114
|
+
const [timeLeft, setTimeLeft] = useState(0);
|
|
1115
|
+
|
|
1116
|
+
// Start the device flow
|
|
1117
|
+
const startFlow = useCallback(async () => {
|
|
1118
|
+
setState({ step: 'loading' });
|
|
1119
|
+
|
|
1120
|
+
try {
|
|
1121
|
+
const res = await fetch(`/api/device-flow/${provider}/start`, {
|
|
1122
|
+
method: 'POST',
|
|
1123
|
+
credentials: 'include'
|
|
1124
|
+
});
|
|
1125
|
+
const data = await res.json();
|
|
1126
|
+
|
|
1127
|
+
if (!res.ok) throw new Error(data.error);
|
|
1128
|
+
|
|
1129
|
+
setState({
|
|
1130
|
+
step: 'showing_code',
|
|
1131
|
+
flowId: data.flowId,
|
|
1132
|
+
userCode: data.userCode,
|
|
1133
|
+
verificationUri: data.verificationUri,
|
|
1134
|
+
expiresAt: new Date(Date.now() + data.expiresIn * 1000)
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// Open provider auth page
|
|
1138
|
+
window.open(
|
|
1139
|
+
data.verificationUriComplete || data.verificationUri,
|
|
1140
|
+
'_blank',
|
|
1141
|
+
'noopener'
|
|
1142
|
+
);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
setState({
|
|
1145
|
+
step: 'error',
|
|
1146
|
+
message: error.message,
|
|
1147
|
+
canRetry: true
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}, [provider]);
|
|
1151
|
+
|
|
1152
|
+
// Poll for status
|
|
1153
|
+
useEffect(() => {
|
|
1154
|
+
if (state.step !== 'showing_code') return;
|
|
1155
|
+
|
|
1156
|
+
const checkStatus = async () => {
|
|
1157
|
+
try {
|
|
1158
|
+
const res = await fetch(
|
|
1159
|
+
`/api/device-flow/${provider}/status/${state.flowId}`,
|
|
1160
|
+
{ credentials: 'include' }
|
|
1161
|
+
);
|
|
1162
|
+
const data = await res.json();
|
|
1163
|
+
|
|
1164
|
+
switch (data.status) {
|
|
1165
|
+
case 'success':
|
|
1166
|
+
setState({ step: 'success' });
|
|
1167
|
+
setTimeout(onSuccess, 1500);
|
|
1168
|
+
break;
|
|
1169
|
+
case 'expired':
|
|
1170
|
+
setState({
|
|
1171
|
+
step: 'error',
|
|
1172
|
+
message: 'Code expired. Please try again.',
|
|
1173
|
+
canRetry: true
|
|
1174
|
+
});
|
|
1175
|
+
break;
|
|
1176
|
+
case 'denied':
|
|
1177
|
+
setState({
|
|
1178
|
+
step: 'error',
|
|
1179
|
+
message: 'Authorization was denied.',
|
|
1180
|
+
canRetry: true
|
|
1181
|
+
});
|
|
1182
|
+
break;
|
|
1183
|
+
case 'error':
|
|
1184
|
+
setState({
|
|
1185
|
+
step: 'error',
|
|
1186
|
+
message: data.error || 'An error occurred.',
|
|
1187
|
+
canRetry: true
|
|
1188
|
+
});
|
|
1189
|
+
break;
|
|
1190
|
+
// 'pending' - keep polling
|
|
1191
|
+
}
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
console.error('Status check failed:', error);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
const interval = setInterval(checkStatus, 2000);
|
|
1198
|
+
return () => clearInterval(interval);
|
|
1199
|
+
}, [state, provider, onSuccess]);
|
|
1200
|
+
|
|
1201
|
+
// Countdown timer
|
|
1202
|
+
useEffect(() => {
|
|
1203
|
+
if (state.step !== 'showing_code') return;
|
|
1204
|
+
|
|
1205
|
+
const tick = () => {
|
|
1206
|
+
const remaining = Math.floor(
|
|
1207
|
+
(state.expiresAt.getTime() - Date.now()) / 1000
|
|
1208
|
+
);
|
|
1209
|
+
setTimeLeft(Math.max(0, remaining));
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
tick();
|
|
1213
|
+
const interval = setInterval(tick, 1000);
|
|
1214
|
+
return () => clearInterval(interval);
|
|
1215
|
+
}, [state]);
|
|
1216
|
+
|
|
1217
|
+
const formatTime = (s: number) =>
|
|
1218
|
+
`${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;
|
|
1219
|
+
|
|
1220
|
+
const copyCode = () => {
|
|
1221
|
+
if (state.step === 'showing_code') {
|
|
1222
|
+
navigator.clipboard.writeText(state.userCode);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
// Render
|
|
1227
|
+
switch (state.step) {
|
|
1228
|
+
case 'ready':
|
|
1229
|
+
return (
|
|
1230
|
+
<div className="device-flow">
|
|
1231
|
+
<h2>Connect {providerName}</h2>
|
|
1232
|
+
<p>
|
|
1233
|
+
Click below to sign in with your {providerName} account.
|
|
1234
|
+
You'll enter a code to link your account.
|
|
1235
|
+
</p>
|
|
1236
|
+
<div className="actions">
|
|
1237
|
+
<button onClick={startFlow} className="primary">
|
|
1238
|
+
Open {providerName} →
|
|
1239
|
+
</button>
|
|
1240
|
+
<button onClick={onCancel} className="secondary">
|
|
1241
|
+
Cancel
|
|
1242
|
+
</button>
|
|
1243
|
+
</div>
|
|
1244
|
+
</div>
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
case 'loading':
|
|
1248
|
+
return (
|
|
1249
|
+
<div className="device-flow loading">
|
|
1250
|
+
<div className="spinner" />
|
|
1251
|
+
<p>Preparing authorization...</p>
|
|
1252
|
+
</div>
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
case 'showing_code':
|
|
1256
|
+
return (
|
|
1257
|
+
<div className="device-flow">
|
|
1258
|
+
<h2>Enter this code at {providerName}</h2>
|
|
1259
|
+
|
|
1260
|
+
<div className="code-box">
|
|
1261
|
+
<code className="user-code">{state.userCode}</code>
|
|
1262
|
+
<button onClick={copyCode} className="copy-btn">
|
|
1263
|
+
Copy
|
|
1264
|
+
</button>
|
|
1265
|
+
</div>
|
|
1266
|
+
|
|
1267
|
+
<p className="hint">
|
|
1268
|
+
A browser tab opened to{' '}
|
|
1269
|
+
<a href={state.verificationUri} target="_blank" rel="noopener">
|
|
1270
|
+
{new URL(state.verificationUri).hostname}
|
|
1271
|
+
</a>
|
|
1272
|
+
</p>
|
|
1273
|
+
|
|
1274
|
+
<div className="status">
|
|
1275
|
+
<span className="spinner small" />
|
|
1276
|
+
<span>Waiting for authorization...</span>
|
|
1277
|
+
</div>
|
|
1278
|
+
|
|
1279
|
+
<div className="timer">
|
|
1280
|
+
Code expires in {formatTime(timeLeft)}
|
|
1281
|
+
</div>
|
|
1282
|
+
|
|
1283
|
+
<button onClick={onCancel} className="secondary">
|
|
1284
|
+
Cancel
|
|
1285
|
+
</button>
|
|
1286
|
+
</div>
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
case 'success':
|
|
1290
|
+
return (
|
|
1291
|
+
<div className="device-flow success">
|
|
1292
|
+
<div className="icon">✅</div>
|
|
1293
|
+
<h2>Connected!</h2>
|
|
1294
|
+
<p>Your {providerName} account is now linked.</p>
|
|
1295
|
+
</div>
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
case 'error':
|
|
1299
|
+
return (
|
|
1300
|
+
<div className="device-flow error">
|
|
1301
|
+
<div className="icon">❌</div>
|
|
1302
|
+
<h2>Connection Failed</h2>
|
|
1303
|
+
<p>{state.message}</p>
|
|
1304
|
+
<div className="actions">
|
|
1305
|
+
{state.canRetry && (
|
|
1306
|
+
<button onClick={startFlow} className="primary">
|
|
1307
|
+
Try Again
|
|
1308
|
+
</button>
|
|
1309
|
+
)}
|
|
1310
|
+
<button onClick={onCancel} className="secondary">
|
|
1311
|
+
Cancel
|
|
1312
|
+
</button>
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
---
|
|
1321
|
+
|
|
1322
|
+
#### Credential Import (Alternative for Claude)
|
|
1323
|
+
|
|
1324
|
+
For users who prefer to authenticate locally first:
|
|
1325
|
+
|
|
1326
|
+
```
|
|
1327
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1328
|
+
│ Connect Claude Code │
|
|
1329
|
+
│ │
|
|
1330
|
+
│ Choose how to connect: │
|
|
1331
|
+
│ │
|
|
1332
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
1333
|
+
│ │ 🔐 Login with Anthropic ││
|
|
1334
|
+
│ │ Authenticate in browser (recommended) ││
|
|
1335
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
1336
|
+
│ │
|
|
1337
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
1338
|
+
│ │ 📁 Import from Local Claude ││
|
|
1339
|
+
│ │ Already have Claude Code installed? Import credentials ││
|
|
1340
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
1341
|
+
│ │
|
|
1342
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
Import flow:
|
|
1346
|
+
|
|
1347
|
+
```
|
|
1348
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1349
|
+
│ Import Claude Credentials │
|
|
1350
|
+
│ │
|
|
1351
|
+
│ Run this command on your local machine: │
|
|
1352
|
+
│ │
|
|
1353
|
+
│ ┌─────────────────────────────────────────────────────────────┐│
|
|
1354
|
+
│ │ npx agent-relay-cloud export-credentials ││
|
|
1355
|
+
│ └─────────────────────────────────────────────────────────────┘│
|
|
1356
|
+
│ [Copy] │
|
|
1357
|
+
│ │
|
|
1358
|
+
│ This will: │
|
|
1359
|
+
│ • Read your Claude credentials from ~/.claude/ │
|
|
1360
|
+
│ • Encrypt them with a one-time code │
|
|
1361
|
+
│ • Upload securely to Agent Relay Cloud │
|
|
1362
|
+
│ │
|
|
1363
|
+
│ Your credentials never leave your machine unencrypted. │
|
|
1364
|
+
│ │
|
|
1365
|
+
│ ⏳ Waiting for import... │
|
|
1366
|
+
│ │
|
|
1367
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
### Step 4: Configure Your First Team (Optional)
|
|
1371
|
+
|
|
1372
|
+
```
|
|
1373
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1374
|
+
│ Create Your First Agent Team │
|
|
1375
|
+
│ │
|
|
1376
|
+
│ Teams are groups of AI agents that work together on tasks. │
|
|
1377
|
+
│ │
|
|
1378
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
1379
|
+
│ │ 🚀 Quick Start Templates │ │
|
|
1380
|
+
│ ├──────────────────────────────────────────────────────────┤ │
|
|
1381
|
+
│ │ │ │
|
|
1382
|
+
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
|
1383
|
+
│ │ │ 👥 Code Review Team │ │ │
|
|
1384
|
+
│ │ │ Architect + Reviewer + Security Auditor │ │ │
|
|
1385
|
+
│ │ │ Auto-reviews PRs and suggests improvements │ │ │
|
|
1386
|
+
│ │ └────────────────────────────────────────────────────┘ │ │
|
|
1387
|
+
│ │ │ │
|
|
1388
|
+
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
|
1389
|
+
│ │ │ 🛠️ Feature Development Team │ │ │
|
|
1390
|
+
│ │ │ Lead + Frontend + Backend + Tester │ │ │
|
|
1391
|
+
│ │ │ Coordinates multi-agent feature builds │ │ │
|
|
1392
|
+
│ │ └────────────────────────────────────────────────────┘ │ │
|
|
1393
|
+
│ │ │ │
|
|
1394
|
+
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
|
1395
|
+
│ │ │ 📝 Custom Team │ │ │
|
|
1396
|
+
│ │ │ Configure your own agent composition │ │ │
|
|
1397
|
+
│ │ └────────────────────────────────────────────────────┘ │ │
|
|
1398
|
+
│ │ │ │
|
|
1399
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
1400
|
+
│ │
|
|
1401
|
+
│ ┌──────────────┐ ┌──────────────────────────────────────────┐ │
|
|
1402
|
+
│ │ Skip for now │ │ Select template → │ │
|
|
1403
|
+
│ └──────────────┘ └──────────────────────────────────────────┘ │
|
|
1404
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
### Step 5: Ready to Go!
|
|
1408
|
+
|
|
1409
|
+
```
|
|
1410
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1411
|
+
│ 🎉 You're all set! │
|
|
1412
|
+
│ │
|
|
1413
|
+
│ Your Agent Relay Cloud workspace is ready: │
|
|
1414
|
+
│ │
|
|
1415
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
1416
|
+
│ │ 📂 Repositories 2 connected │ │
|
|
1417
|
+
│ │ 🤖 Agent Providers Claude, Codex │ │
|
|
1418
|
+
│ │ 👥 Teams Code Review Team │ │
|
|
1419
|
+
│ │ 🌐 Dashboard relay.yourdomain.cloud │ │
|
|
1420
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
1421
|
+
│ │
|
|
1422
|
+
│ What's next? │
|
|
1423
|
+
│ │
|
|
1424
|
+
│ • Open a PR to trigger automatic code review │
|
|
1425
|
+
│ • Use @agent-relay in PR comments to chat with agents │
|
|
1426
|
+
│ • Visit your dashboard to monitor agent activity │
|
|
1427
|
+
│ │
|
|
1428
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
1429
|
+
│ │ Open Dashboard → │ │
|
|
1430
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
1431
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
---
|
|
1435
|
+
|
|
1436
|
+
## Technical Implementation
|
|
1437
|
+
|
|
1438
|
+
### Provider Credentials Storage
|
|
1439
|
+
|
|
1440
|
+
```typescript
|
|
1441
|
+
// src/cloud/providers/types.ts
|
|
1442
|
+
|
|
1443
|
+
interface ProviderCredential {
|
|
1444
|
+
id: string;
|
|
1445
|
+
userId: string;
|
|
1446
|
+
provider: ProviderType;
|
|
1447
|
+
|
|
1448
|
+
// OAuth tokens (encrypted at rest)
|
|
1449
|
+
accessToken: string;
|
|
1450
|
+
refreshToken: string;
|
|
1451
|
+
tokenExpiresAt: Date;
|
|
1452
|
+
scopes: string[];
|
|
1453
|
+
|
|
1454
|
+
// Account info from provider
|
|
1455
|
+
providerAccountId: string; // Provider's user ID
|
|
1456
|
+
providerAccountEmail: string; // For display: "user@example.com"
|
|
1457
|
+
providerAccountName?: string; // Display name if available
|
|
1458
|
+
|
|
1459
|
+
// Metadata
|
|
1460
|
+
connectedAt: Date;
|
|
1461
|
+
lastUsedAt?: Date;
|
|
1462
|
+
lastRefreshedAt?: Date;
|
|
1463
|
+
isValid: boolean;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
type ProviderType =
|
|
1467
|
+
| 'anthropic' // Claude Code
|
|
1468
|
+
| 'openai' // Codex, ChatGPT
|
|
1469
|
+
| 'google' // Gemini
|
|
1470
|
+
| 'github' // Copilot (auto-connected via signup)
|
|
1471
|
+
| 'microsoft' // Azure OpenAI
|
|
1472
|
+
| 'self-hosted'; // Ollama, LM Studio (no auth needed)
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
### Provider Registry
|
|
1476
|
+
|
|
1477
|
+
```typescript
|
|
1478
|
+
// src/cloud/providers/registry.ts
|
|
1479
|
+
|
|
1480
|
+
interface OAuthConfig {
|
|
1481
|
+
authorizationUrl: string;
|
|
1482
|
+
tokenUrl: string;
|
|
1483
|
+
scopes: string[];
|
|
1484
|
+
userInfoUrl?: string; // To fetch account email/name after auth
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
interface ProviderConfig {
|
|
1488
|
+
id: ProviderType;
|
|
1489
|
+
name: string;
|
|
1490
|
+
displayName: string; // "Login with {displayName}"
|
|
1491
|
+
description: string;
|
|
1492
|
+
cliCommand: string;
|
|
1493
|
+
cliArgs?: string[];
|
|
1494
|
+
oauthConfig: OAuthConfig;
|
|
1495
|
+
icon: string;
|
|
1496
|
+
color: string; // Brand color for button
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const PROVIDER_REGISTRY: ProviderConfig[] = [
|
|
1500
|
+
{
|
|
1501
|
+
id: 'anthropic',
|
|
1502
|
+
name: 'Anthropic',
|
|
1503
|
+
displayName: 'Anthropic',
|
|
1504
|
+
description: 'Claude Code - recommended for code tasks',
|
|
1505
|
+
cliCommand: 'claude',
|
|
1506
|
+
cliArgs: ['--dangerously-skip-permissions'],
|
|
1507
|
+
oauthConfig: {
|
|
1508
|
+
authorizationUrl: 'https://console.anthropic.com/oauth/authorize',
|
|
1509
|
+
tokenUrl: 'https://api.anthropic.com/oauth/token',
|
|
1510
|
+
scopes: ['claude-code:execute', 'user:read'],
|
|
1511
|
+
userInfoUrl: 'https://api.anthropic.com/v1/user'
|
|
1512
|
+
},
|
|
1513
|
+
icon: 'anthropic-logo.svg',
|
|
1514
|
+
color: '#D97757'
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
id: 'openai',
|
|
1518
|
+
name: 'OpenAI',
|
|
1519
|
+
displayName: 'OpenAI',
|
|
1520
|
+
description: 'Codex and ChatGPT models',
|
|
1521
|
+
cliCommand: 'codex',
|
|
1522
|
+
cliArgs: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
1523
|
+
oauthConfig: {
|
|
1524
|
+
authorizationUrl: 'https://auth.openai.com/authorize',
|
|
1525
|
+
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
1526
|
+
scopes: ['openid', 'profile', 'email', 'model.read', 'model.request'],
|
|
1527
|
+
userInfoUrl: 'https://api.openai.com/v1/user'
|
|
1528
|
+
},
|
|
1529
|
+
icon: 'openai-logo.svg',
|
|
1530
|
+
color: '#10A37F'
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
id: 'google',
|
|
1534
|
+
name: 'Google',
|
|
1535
|
+
displayName: 'Google',
|
|
1536
|
+
description: 'Gemini - multi-modal capabilities',
|
|
1537
|
+
cliCommand: 'gemini',
|
|
1538
|
+
oauthConfig: {
|
|
1539
|
+
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
1540
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
1541
|
+
scopes: [
|
|
1542
|
+
'openid',
|
|
1543
|
+
'email',
|
|
1544
|
+
'profile',
|
|
1545
|
+
'https://www.googleapis.com/auth/generative-language'
|
|
1546
|
+
],
|
|
1547
|
+
userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo'
|
|
1548
|
+
},
|
|
1549
|
+
icon: 'google-logo.svg',
|
|
1550
|
+
color: '#4285F4'
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
id: 'github',
|
|
1554
|
+
name: 'GitHub',
|
|
1555
|
+
displayName: 'GitHub',
|
|
1556
|
+
description: 'Copilot - auto-connected via signup',
|
|
1557
|
+
cliCommand: 'gh-copilot',
|
|
1558
|
+
oauthConfig: {
|
|
1559
|
+
// Uses same OAuth from signup - just needs Copilot scope
|
|
1560
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
1561
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
1562
|
+
scopes: ['copilot', 'read:user', 'user:email'],
|
|
1563
|
+
userInfoUrl: 'https://api.github.com/user'
|
|
1564
|
+
},
|
|
1565
|
+
icon: 'github-logo.svg',
|
|
1566
|
+
color: '#24292F'
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
id: 'microsoft',
|
|
1570
|
+
name: 'Microsoft',
|
|
1571
|
+
displayName: 'Microsoft',
|
|
1572
|
+
description: 'Azure OpenAI - enterprise deployments',
|
|
1573
|
+
cliCommand: 'azure-openai',
|
|
1574
|
+
oauthConfig: {
|
|
1575
|
+
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
|
1576
|
+
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
|
1577
|
+
scopes: [
|
|
1578
|
+
'openid',
|
|
1579
|
+
'profile',
|
|
1580
|
+
'email',
|
|
1581
|
+
'https://cognitiveservices.azure.com/.default'
|
|
1582
|
+
],
|
|
1583
|
+
userInfoUrl: 'https://graph.microsoft.com/v1.0/me'
|
|
1584
|
+
},
|
|
1585
|
+
icon: 'microsoft-logo.svg',
|
|
1586
|
+
color: '#00A4EF'
|
|
1587
|
+
}
|
|
1588
|
+
];
|
|
1589
|
+
|
|
1590
|
+
// Self-hosted providers don't need OAuth
|
|
1591
|
+
interface SelfHostedProvider {
|
|
1592
|
+
id: 'self-hosted';
|
|
1593
|
+
name: string;
|
|
1594
|
+
endpoint: string; // e.g., "http://localhost:11434" for Ollama
|
|
1595
|
+
cliCommand: string;
|
|
1596
|
+
}
|
|
1597
|
+
```
|
|
1598
|
+
|
|
1599
|
+
### Spawner Integration
|
|
1600
|
+
|
|
1601
|
+
```typescript
|
|
1602
|
+
// src/cloud/spawner-cloud.ts
|
|
1603
|
+
|
|
1604
|
+
class CloudAgentSpawner extends AgentSpawner {
|
|
1605
|
+
private credentialVault: CredentialVault;
|
|
1606
|
+
private tokenRefresher: TokenRefresher;
|
|
1607
|
+
|
|
1608
|
+
async spawn(request: CloudSpawnRequest): Promise<SpawnedAgent> {
|
|
1609
|
+
const { userId, provider, agentName, task } = request;
|
|
1610
|
+
|
|
1611
|
+
// Get credentials for this provider
|
|
1612
|
+
const credential = await this.credentialVault.get(userId, provider);
|
|
1613
|
+
if (!credential) {
|
|
1614
|
+
throw new ProviderNotConnectedError(provider);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Refresh token if expired or expiring soon (within 5 min)
|
|
1618
|
+
const validCredential = await this.ensureValidToken(credential);
|
|
1619
|
+
|
|
1620
|
+
// Build environment with OAuth token
|
|
1621
|
+
const env = this.buildProviderEnv(validCredential);
|
|
1622
|
+
|
|
1623
|
+
// Get provider config
|
|
1624
|
+
const providerConfig = PROVIDER_REGISTRY.find(p => p.id === provider);
|
|
1625
|
+
|
|
1626
|
+
// Spawn agent with credentials injected
|
|
1627
|
+
return super.spawn({
|
|
1628
|
+
name: agentName,
|
|
1629
|
+
cli: providerConfig.cliCommand,
|
|
1630
|
+
args: providerConfig.cliArgs,
|
|
1631
|
+
env,
|
|
1632
|
+
task
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
private async ensureValidToken(credential: ProviderCredential): Promise<ProviderCredential> {
|
|
1637
|
+
const expiresIn = credential.tokenExpiresAt.getTime() - Date.now();
|
|
1638
|
+
const FIVE_MINUTES = 5 * 60 * 1000;
|
|
1639
|
+
|
|
1640
|
+
if (expiresIn < FIVE_MINUTES) {
|
|
1641
|
+
// Refresh the token
|
|
1642
|
+
const provider = PROVIDER_REGISTRY.find(p => p.id === credential.provider);
|
|
1643
|
+
const newTokens = await this.tokenRefresher.refresh(
|
|
1644
|
+
provider.oauthConfig,
|
|
1645
|
+
credential.refreshToken
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
// Update stored credential
|
|
1649
|
+
const updated = await this.credentialVault.update(credential.id, {
|
|
1650
|
+
accessToken: newTokens.accessToken,
|
|
1651
|
+
refreshToken: newTokens.refreshToken ?? credential.refreshToken,
|
|
1652
|
+
tokenExpiresAt: newTokens.expiresAt,
|
|
1653
|
+
lastRefreshedAt: new Date()
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
return updated;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return credential;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
private buildProviderEnv(credential: ProviderCredential): Record<string, string> {
|
|
1663
|
+
// Each provider expects OAuth token in different env vars
|
|
1664
|
+
const envMapping: Record<ProviderType, string> = {
|
|
1665
|
+
'anthropic': 'ANTHROPIC_AUTH_TOKEN',
|
|
1666
|
+
'openai': 'OPENAI_AUTH_TOKEN',
|
|
1667
|
+
'google': 'GOOGLE_AUTH_TOKEN',
|
|
1668
|
+
'github': 'GITHUB_TOKEN',
|
|
1669
|
+
'microsoft': 'AZURE_AUTH_TOKEN',
|
|
1670
|
+
'self-hosted': '' // No auth needed
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
const envVar = envMapping[credential.provider];
|
|
1674
|
+
if (!envVar) return {};
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
[envVar]: credential.accessToken,
|
|
1678
|
+
// Some CLIs also want the account info
|
|
1679
|
+
'PROVIDER_ACCOUNT_EMAIL': credential.providerAccountEmail
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
class ProviderNotConnectedError extends Error {
|
|
1685
|
+
constructor(provider: ProviderType) {
|
|
1686
|
+
super(`Provider "${provider}" is not connected. Please connect it in Settings.`);
|
|
1687
|
+
this.name = 'ProviderNotConnectedError';
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
### Onboarding API
|
|
1693
|
+
|
|
1694
|
+
```typescript
|
|
1695
|
+
// src/cloud/api/onboarding.ts
|
|
1696
|
+
|
|
1697
|
+
const onboardingRouter = Router();
|
|
1698
|
+
|
|
1699
|
+
// Step 1: GitHub OAuth callback (primary signup/login)
|
|
1700
|
+
onboardingRouter.get('/auth/github/callback', async (req, res) => {
|
|
1701
|
+
const { code } = req.query;
|
|
1702
|
+
const tokens = await exchangeGitHubCode(code);
|
|
1703
|
+
const user = await createOrUpdateUser(tokens);
|
|
1704
|
+
|
|
1705
|
+
// Store GitHub credential (also gives us Copilot access)
|
|
1706
|
+
await credentialVault.store({
|
|
1707
|
+
userId: user.id,
|
|
1708
|
+
provider: 'github',
|
|
1709
|
+
accessToken: tokens.accessToken,
|
|
1710
|
+
refreshToken: tokens.refreshToken,
|
|
1711
|
+
tokenExpiresAt: tokens.expiresAt,
|
|
1712
|
+
scopes: tokens.scopes,
|
|
1713
|
+
providerAccountId: tokens.user.id,
|
|
1714
|
+
providerAccountEmail: tokens.user.email,
|
|
1715
|
+
providerAccountName: tokens.user.name,
|
|
1716
|
+
connectedAt: new Date(),
|
|
1717
|
+
isValid: true
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
// Set session and redirect to repo selection
|
|
1721
|
+
req.session.userId = user.id;
|
|
1722
|
+
res.redirect('/onboarding/repositories');
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
// Step 2: Get user's repositories
|
|
1726
|
+
onboardingRouter.get('/repositories', async (req, res) => {
|
|
1727
|
+
const repos = await github.listUserRepos(req.session.accessToken);
|
|
1728
|
+
res.json({ repos });
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
// Step 2: Connect selected repositories
|
|
1732
|
+
onboardingRouter.post('/repositories', async (req, res) => {
|
|
1733
|
+
const { repoIds } = req.body;
|
|
1734
|
+
await Promise.all(repoIds.map(id =>
|
|
1735
|
+
connectRepository(req.session.userId, id)
|
|
1736
|
+
));
|
|
1737
|
+
res.json({ success: true });
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// Step 3: List available providers with connection status
|
|
1741
|
+
onboardingRouter.get('/providers', async (req, res) => {
|
|
1742
|
+
const connected = await getConnectedProviders(req.session.userId);
|
|
1743
|
+
|
|
1744
|
+
// Map registry with connection status
|
|
1745
|
+
const providers = PROVIDER_REGISTRY.map(p => ({
|
|
1746
|
+
...p,
|
|
1747
|
+
isConnected: connected.some(c => c.provider === p.id),
|
|
1748
|
+
connectedAs: connected.find(c => c.provider === p.id)?.providerAccountEmail
|
|
1749
|
+
}));
|
|
1750
|
+
|
|
1751
|
+
res.json({ providers });
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
// Step 3: Initiate OAuth login for a provider
|
|
1755
|
+
onboardingRouter.get('/providers/:provider/login', async (req, res) => {
|
|
1756
|
+
const { provider } = req.params;
|
|
1757
|
+
const config = PROVIDER_REGISTRY.find(p => p.id === provider);
|
|
1758
|
+
|
|
1759
|
+
if (!config) {
|
|
1760
|
+
return res.status(404).json({ error: 'Unknown provider' });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// Generate state token for CSRF protection
|
|
1764
|
+
const state = await generateOAuthState({
|
|
1765
|
+
userId: req.session.userId,
|
|
1766
|
+
provider,
|
|
1767
|
+
returnTo: req.query.returnTo || '/onboarding/providers'
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
// Build OAuth authorization URL
|
|
1771
|
+
const authUrl = new URL(config.oauthConfig.authorizationUrl);
|
|
1772
|
+
authUrl.searchParams.set('client_id', getClientId(provider));
|
|
1773
|
+
authUrl.searchParams.set('redirect_uri', `${BASE_URL}/providers/${provider}/callback`);
|
|
1774
|
+
authUrl.searchParams.set('scope', config.oauthConfig.scopes.join(' '));
|
|
1775
|
+
authUrl.searchParams.set('state', state);
|
|
1776
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
1777
|
+
|
|
1778
|
+
res.redirect(authUrl.toString());
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
// Step 3: OAuth callback after user authorizes
|
|
1782
|
+
onboardingRouter.get('/providers/:provider/callback', async (req, res) => {
|
|
1783
|
+
const { code, state, error } = req.query;
|
|
1784
|
+
|
|
1785
|
+
if (error) {
|
|
1786
|
+
return res.redirect(`/onboarding/providers?error=${error}`);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Verify state token
|
|
1790
|
+
const stateData = await verifyOAuthState(state);
|
|
1791
|
+
if (!stateData) {
|
|
1792
|
+
return res.status(400).json({ error: 'Invalid state' });
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const { userId, provider, returnTo } = stateData;
|
|
1796
|
+
const config = PROVIDER_REGISTRY.find(p => p.id === provider);
|
|
1797
|
+
|
|
1798
|
+
// Exchange code for tokens
|
|
1799
|
+
const tokens = await exchangeOAuthCode({
|
|
1800
|
+
tokenUrl: config.oauthConfig.tokenUrl,
|
|
1801
|
+
code,
|
|
1802
|
+
clientId: getClientId(provider),
|
|
1803
|
+
clientSecret: getClientSecret(provider),
|
|
1804
|
+
redirectUri: `${BASE_URL}/providers/${provider}/callback`
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
// Fetch user info from provider
|
|
1808
|
+
const userInfo = await fetchProviderUserInfo(
|
|
1809
|
+
config.oauthConfig.userInfoUrl,
|
|
1810
|
+
tokens.accessToken
|
|
1811
|
+
);
|
|
1812
|
+
|
|
1813
|
+
// Store credential
|
|
1814
|
+
await credentialVault.store({
|
|
1815
|
+
userId,
|
|
1816
|
+
provider,
|
|
1817
|
+
accessToken: tokens.accessToken,
|
|
1818
|
+
refreshToken: tokens.refreshToken,
|
|
1819
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expiresIn * 1000),
|
|
1820
|
+
scopes: tokens.scope.split(' '),
|
|
1821
|
+
providerAccountId: userInfo.id,
|
|
1822
|
+
providerAccountEmail: userInfo.email,
|
|
1823
|
+
providerAccountName: userInfo.name,
|
|
1824
|
+
connectedAt: new Date(),
|
|
1825
|
+
isValid: true
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
res.redirect(`${returnTo}?connected=${provider}`);
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
// Step 3: Disconnect a provider
|
|
1832
|
+
onboardingRouter.delete('/providers/:provider', async (req, res) => {
|
|
1833
|
+
const { provider } = req.params;
|
|
1834
|
+
|
|
1835
|
+
await credentialVault.delete(req.session.userId, provider);
|
|
1836
|
+
|
|
1837
|
+
res.json({ success: true });
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
// Step 4: Create team from template
|
|
1841
|
+
onboardingRouter.post('/teams/from-template', async (req, res) => {
|
|
1842
|
+
const { templateId, repoIds, defaultProvider } = req.body;
|
|
1843
|
+
const template = TEAM_TEMPLATES[templateId];
|
|
1844
|
+
|
|
1845
|
+
// Verify user has the default provider connected
|
|
1846
|
+
const hasProvider = await credentialVault.exists(
|
|
1847
|
+
req.session.userId,
|
|
1848
|
+
defaultProvider || 'anthropic'
|
|
1849
|
+
);
|
|
1850
|
+
|
|
1851
|
+
if (!hasProvider) {
|
|
1852
|
+
return res.status(400).json({
|
|
1853
|
+
error: 'Provider not connected',
|
|
1854
|
+
message: `Please connect ${defaultProvider || 'Anthropic'} first`
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
const team = await createTeam({
|
|
1859
|
+
userId: req.session.userId,
|
|
1860
|
+
name: template.name,
|
|
1861
|
+
agents: template.agents.map(a => ({
|
|
1862
|
+
...a,
|
|
1863
|
+
provider: defaultProvider || 'anthropic'
|
|
1864
|
+
})),
|
|
1865
|
+
repoIds
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
res.json({ team });
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// Complete onboarding
|
|
1872
|
+
onboardingRouter.post('/complete', async (req, res) => {
|
|
1873
|
+
await markOnboardingComplete(req.session.userId);
|
|
1874
|
+
|
|
1875
|
+
// Provision workspace
|
|
1876
|
+
const workspace = await provisionWorkspace(req.session.userId);
|
|
1877
|
+
|
|
1878
|
+
res.json({
|
|
1879
|
+
dashboardUrl: workspace.dashboardUrl,
|
|
1880
|
+
webhookUrl: workspace.webhookUrl
|
|
1881
|
+
});
|
|
1882
|
+
});
|
|
1883
|
+
```
|
|
1884
|
+
|
|
1885
|
+
---
|
|
1886
|
+
|
|
1887
|
+
## Security Considerations
|
|
1888
|
+
|
|
1889
|
+
### OAuth Token Security
|
|
1890
|
+
|
|
1891
|
+
1. **Encryption at rest**: All tokens encrypted with AES-256-GCM
|
|
1892
|
+
2. **Key derivation**: Per-user encryption keys derived from master key + user ID
|
|
1893
|
+
3. **No plaintext logging**: Tokens never logged, even in debug mode
|
|
1894
|
+
4. **Short-lived access tokens**: Rely on refresh tokens for long sessions
|
|
1895
|
+
|
|
1896
|
+
### Token Lifecycle Management
|
|
1897
|
+
|
|
1898
|
+
1. **Automatic refresh**: Tokens refreshed 5 minutes before expiry
|
|
1899
|
+
2. **Refresh token rotation**: Use rotating refresh tokens where supported
|
|
1900
|
+
3. **Revocation detection**: Check token validity on spawn, prompt re-auth if revoked
|
|
1901
|
+
4. **Graceful degradation**: Queue tasks if token refresh fails temporarily
|
|
1902
|
+
|
|
1903
|
+
### Scope Management
|
|
1904
|
+
|
|
1905
|
+
1. **Minimum scopes**: Request only scopes needed for agent execution
|
|
1906
|
+
2. **Scope display**: Show users exactly what permissions we request
|
|
1907
|
+
3. **No scope creep**: Never silently request additional scopes
|
|
1908
|
+
|
|
1909
|
+
### Access Control
|
|
1910
|
+
|
|
1911
|
+
1. **User isolation**: Credentials tied to user ID, never shared
|
|
1912
|
+
2. **Team permissions**: Team admins can enable provider access for team members
|
|
1913
|
+
3. **Audit logging**: All credential access and agent spawns logged
|
|
1914
|
+
4. **Rate limiting**: Provider usage rate-limited per user/team
|
|
1915
|
+
|
|
1916
|
+
---
|
|
1917
|
+
|
|
1918
|
+
## Future Enhancements
|
|
1919
|
+
|
|
1920
|
+
### 1. Credential Sharing for Teams
|
|
1921
|
+
|
|
1922
|
+
Allow team admins to share provider credentials with team members:
|
|
1923
|
+
|
|
1924
|
+
```typescript
|
|
1925
|
+
interface SharedCredential {
|
|
1926
|
+
credentialId: string;
|
|
1927
|
+
teamId: string;
|
|
1928
|
+
sharedBy: string;
|
|
1929
|
+
permissions: 'read' | 'use'; // 'use' allows spawning agents
|
|
1930
|
+
}
|
|
1931
|
+
```
|
|
1932
|
+
|
|
1933
|
+
### 2. Usage Tracking & Billing
|
|
1934
|
+
|
|
1935
|
+
Track provider usage per user/team for billing:
|
|
1936
|
+
|
|
1937
|
+
```typescript
|
|
1938
|
+
interface UsageRecord {
|
|
1939
|
+
userId: string;
|
|
1940
|
+
teamId?: string;
|
|
1941
|
+
provider: ProviderType;
|
|
1942
|
+
agentName: string;
|
|
1943
|
+
tokensUsed: number;
|
|
1944
|
+
duration: number;
|
|
1945
|
+
timestamp: Date;
|
|
1946
|
+
}
|
|
1947
|
+
```
|
|
1948
|
+
|
|
1949
|
+
### 3. Provider Health Monitoring
|
|
1950
|
+
|
|
1951
|
+
Monitor provider availability and quota:
|
|
1952
|
+
|
|
1953
|
+
```typescript
|
|
1954
|
+
interface ProviderHealth {
|
|
1955
|
+
provider: ProviderType;
|
|
1956
|
+
status: 'healthy' | 'degraded' | 'down';
|
|
1957
|
+
quotaRemaining?: number;
|
|
1958
|
+
lastChecked: Date;
|
|
1959
|
+
}
|
|
1960
|
+
```
|
|
1961
|
+
|
|
1962
|
+
### 4. Bring Your Own Cloud
|
|
1963
|
+
|
|
1964
|
+
Let users connect their own cloud accounts for compute:
|
|
1965
|
+
|
|
1966
|
+
- AWS credentials for EC2 instances
|
|
1967
|
+
- GCP credentials for Cloud Run
|
|
1968
|
+
- Azure credentials for Container Instances
|
|
1969
|
+
|
|
1970
|
+
---
|
|
1971
|
+
|
|
1972
|
+
## Summary
|
|
1973
|
+
|
|
1974
|
+
The onboarding flow prioritizes:
|
|
1975
|
+
|
|
1976
|
+
1. **Low friction**: GitHub OAuth gets users started immediately
|
|
1977
|
+
2. **Consistent UX**: All providers use "Login with X" - no API keys to manage
|
|
1978
|
+
3. **Security**: OAuth tokens with automatic refresh, encrypted at rest
|
|
1979
|
+
4. **Account linking**: Users log in with their existing provider accounts
|
|
1980
|
+
5. **Progressive disclosure**: Optional team setup, can skip and add later
|
|
1981
|
+
6. **Graceful recovery**: Re-auth prompts when tokens expire or get revoked
|
|
1982
|
+
|
|
1983
|
+
Users can connect all their AI providers during onboarding with simple login buttons, or add them later from Settings. GitHub Copilot is auto-connected via the initial signup flow.
|