Limit GitHub sign-in and correct Coder release

097cb33a22e8 · AtlantisPleb · · parent 0bf2706a8e06

Limit GitHub sign-in and correct Coder release

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 470 · 2026-08-27T05:36:15.803930Z
built
8 modules in 124.1 s
deployed
live · 8 modules on 3 nodes · push→live —
deployed
needs_rolling_replace · 8 modules on 0 nodes · push→live —

Changed files

  • modified config/config.exs
  • modified docs/github-auth-plan.md
  • modified docs/runtime-configuration.md
  • modified lib/openagents/accounts.ex
  • modified lib/openagents/github_oauth.ex
  • modified lib/openagents/runtime_config.ex
  • modified lib/openagents_web/components/ui.ex
  • modified lib/openagents_web/controllers/auth_controller.ex
  • modified lib/openagents_web/route_authority.ex
  • modified ops/deploy/fleet-startup.template.sh
  • modified ops/staging/gate-5-profile.sh
  • modified priv/docs/changelog.md
  • modified test/openagents/accounts_test.exs
  • modified test/openagents/github_oauth_test.exs
  • modified test/openagents/runtime_config_test.exs
  • modified test/openagents_web/auth_controller_test.exs
  • modified test/openagents_web/authenticated_route_gate_test.exs
  • modified test/openagents_web/controllers/data_controller_test.exs
  • modified test/openagents_web/controllers/repository_import_controller_test.exs
  • modified test/openagents_web/device_sign_in_return_test.exs
  • modified test/openagents_web/home_controller_test.exs
  • modified test/openagents_web/live/changelog_live_test.exs
  • modified test/openagents_web/live/repository_live_test.exs
  • modified test/openagents_web/route_authority_test.exs

Diff

24 files changed, +127 -91

config/config.exs modified +1 -1

@@ -455,7 +455,7 @@ config :openagents,

455 455
    base_url: "https://api.github.com",
456 456
    request_options: []
457 457
  ],
458
  github_oauth_scopes: ["repo", "read:org"],
458
  github_oauth_scopes: ["user:email"],
459 459
  voice_recording: [
460 460
    enabled: false,
461 461
    timeslice_ms: 5_000,
docs/github-auth-plan.md modified +23 -31

@@ -8,40 +8,34 @@ Decision: [ADR 0004](decisions/0004-retain-scoped-github-access-tokens.md)

8 8
9 9
## Current contract
10 10
11
GitHub serves two distinct roles:
12
13
1. OAuth establishes the local OpenAgents account identity from GitHub's
14
   immutable numeric user ID.
15
2. A retained access token authorizes server-side GitHub repository tools with
16
   the user's delegated rights.
17
18
The application implements the second model only after the person chooses the
19
button labeled **Sign in and enable GitHub tools** beside a retention
20
disclosure. The callback stores the access token as versioned AES-256-GCM
21
ciphertext in the local user row. It does not discard the token after reading
22
the GitHub profile.
23
24
The OAuth app requests only `repo`. GitHub exposes public profile identity with
25
no OAuth scope, so the redundant `read:user` scope is not requested. The
26
repository tools need to read repositories the user authorizes, including
27
private repositories. GitHub OAuth Apps do not offer read-only source-code
28
access, so `repo` is the narrowest OAuth App scope that satisfies that feature
29
even though the scope grants broad read/write repository and related project
30
rights. The consent UI says so. OpenAgents exposes only its bounded read tools
31
to this credential. A future GitHub App migration should replace this broad scope with
32
fine-grained, repository-selected read permissions. See GitHub's
11
GitHub serves two distinct roles that use separate grants:
12
13
1. Sign-in establishes the local OpenAgents account identity from GitHub's
14
   immutable numeric user ID. It requests only `user:email`.
15
2. A future repository authorization flow will request the separate rights
16
   that server-side GitHub repository tools need.
17
18
The sign-in callback stores the email-scoped token as versioned AES-256-GCM
19
ciphertext in the local user row. It does not authorize repository imports,
20
organization membership checks, or other repository tools. Those operations
21
continue to require the exact `repo,read:org` scope set and fail closed until a
22
separate authorization flow supplies it.
23
24
GitHub exposes public profile identity without a scope. OpenAgents requests
25
`user:email` so sign-in can access the account's email address without asking
26
for repository access. A future GitHub App integration should use fine-grained,
27
repository-selected permissions for repository tools. See GitHub's
33 28
[OAuth scope reference](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)
34 29
and [authorization guidance](https://docs.github.com/en/apps/oauth-apps/using-oauth-apps/authorizing-oauth-apps).
35 30
36 31
## Implemented flow
37 32
38
1. `POST /auth/github?github_tools=enabled` refuses a request that does not
39
   carry the explicit tools choice, then creates a high-entropy state value, a PKCE S256
33
1. `POST /auth/github` creates a high-entropy state value, a PKCE S256
40 34
   challenge, and a short-lived PostgreSQL OAuth-attempt row.
41 35
2. The encrypted browser session carries only the attempt reference and PKCE
42 36
   verifier while GitHub handles authorization.
43 37
3. `GET /auth/github/callback` consumes the attempt exactly once, exchanges the
44
   code server-side, refuses missing or different granted scopes, and reads the
38
   code server-side, refuses any grant other than `user:email`, and reads the
45 39
   GitHub `/user` projection server-side.
46 40
4. `OpenAgents.Accounts` upserts the local account by numeric GitHub ID and
47 41
   refreshes the mutable login, name, and avatar projection.

@@ -49,7 +43,7 @@ and [authorization guidance](https://docs.github.com/en/apps/oauth-apps/using-oa

49 43
   envelope carrying the non-secret active key ID before the ciphertext is
50 44
   stored. The row also records scopes and connection/rotation timestamps.
51 45
6. The authenticated session contains only the local user ID. Repository tools
52
   unseal the token server-side when an explicit GitHub operation needs it.
46
   reject the sign-in token because it does not contain their required scopes.
53 47
7. `DELETE /logout` clears the browser session but intentionally does not
54 48
   revoke the retained GitHub grant.
55 49
8. `DELETE /github/connection` authenticates the browser and uses the OAuth

@@ -80,11 +74,9 @@ Production-mode validation requires an HTTPS callback with the configured

80 74
environment host. Tests use deterministic local configuration and fake Req
81 75
responses; they do not require a live GitHub credential.
82 76
83
Rows migrated from the pre-Gate-6 envelope retain `read:user,repo` metadata
84
because that is the grant they actually received. Before staging admission,
85
revoke those legacy grants and have their owners reconnect under `repo` only;
86
do not rewrite metadata to claim a provider-side scope reduction that did not
87
occur.
77
Existing rows retain their recorded scope metadata because that is the grant
78
they actually received. Do not rewrite metadata to claim a provider-side scope
79
reduction that did not occur.
88 80
89 81
## Rotation and data rights
90 82
docs/runtime-configuration.md modified +1 -1

@@ -150,7 +150,7 @@ arguments, repository URLs, receipts, or checked-in environment files.

150 150
| Database | `OPENAGENTS_MIGRATE_ON_BOOT` | `true` in staging and production |
151 151
| GitHub | `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` | OAuth application credentials for the selected environment |
152 152
| GitHub | `GITHUB_REDIRECT_URI` | Exact HTTPS callback on `PHX_HOST` |
153
| GitHub | `GITHUB_OAUTH_SCOPES` | Exactly `repo,read:org`; repository import needs retained repository access and organization creation needs membership access |
153
| GitHub | `GITHUB_OAUTH_SCOPES` | Exactly `user:email`; sign-in does not request repository access |
154 154
| GitHub | `OPENAGENTS_ADMIN_GITHUB_IDS` | Comma-separated immutable numeric GitHub IDs allowed to use operator surfaces; never use logins |
155 155
| GitHub | `GITHUB_TOKEN_ENCRYPTION_KEY` | Base64-encoded 32-byte key for the selected environment |
156 156
| GitHub | `GITHUB_TOKEN_ENCRYPTION_KEY_ID` | Bounded active-key identifier prefixed with `development-`, `test-`, `staging-`, or `production-` to match the runtime |
lib/openagents/accounts.ex modified +1 -1

@@ -332,6 +332,6 @@ defmodule OpenAgents.Accounts do

332 332
    do: Application.fetch_env!(:openagents, :github_oauth_scopes)
333 333
334 334
  defp valid_scopes?(scopes) do
335
    scopes == configured_github_scopes()
335
    scopes in [configured_github_scopes(), OpenAgents.GitHubOAuth.required_scopes()]
336 336
  end
337 337
end
lib/openagents/github_oauth.ex modified +7 -6

@@ -11,7 +11,8 @@ defmodule OpenAgents.GitHubOAuth do

11 11
  @default_attempt_ttl_seconds 600
12 12
  @github_api_version "2022-11-28"
13 13
  @user_agent "OpenAgents"
14
  @required_scopes ["repo", "read:org"]
14
  @requested_scopes ["user:email"]
15
  @repository_scopes ["repo", "read:org"]
15 16
16 17
  @type attempt :: %{required(String.t()) => String.t() | integer()}
17 18

@@ -107,13 +108,13 @@ defmodule OpenAgents.GitHubOAuth do

107 108
108 109
  def revoke(_access_token), do: {:error, :invalid_token}
109 110
110
  @doc "The exact OAuth scopes retained with each connected GitHub grant."
111
  @doc "The exact OAuth scopes requested during GitHub sign-in."
111 112
  @spec requested_scopes() :: [String.t()]
112
  def requested_scopes, do: @required_scopes
113
  def requested_scopes, do: @requested_scopes
113 114
114
  @doc "The only OAuth scopes admitted by the GitHub-backed namespace and import model."
115
  @doc "The OAuth scopes required by the GitHub-backed namespace and import model."
115 116
  @spec required_scopes() :: [String.t()]
116
  def required_scopes, do: @required_scopes
117
  def required_scopes, do: @repository_scopes
117 118
118 119
  defp exchange_code(config, code, verifier) do
119 120
    request_options =

@@ -252,7 +253,7 @@ defmodule OpenAgents.GitHubOAuth do

252 253
  end
253 254
254 255
  defp validate_scope_configuration do
255
    if Application.get_env(:openagents, :github_oauth_scopes) == @required_scopes,
256
    if Application.get_env(:openagents, :github_oauth_scopes) == @requested_scopes,
256 257
      do: :ok,
257 258
      else: {:error, :github_oauth_scope_configuration_invalid}
258 259
  end
lib/openagents/runtime_config.ex modified +2 -2

@@ -290,9 +290,9 @@ defmodule OpenAgents.RuntimeConfig do

290 290
         :ok <- validate_redirect(redirect_uri, environment),
291 291
         :ok <-
292 292
           ensure(
293
             scopes == ["repo", "read:org"],
293
             scopes == ["user:email"],
294 294
             :github_oauth_scopes,
295
             "must include repository and organization membership access"
295
             "must contain only user email access"
296 296
           ),
297 297
         :ok <-
298 298
           ensure(
lib/openagents_web/components/ui.ex modified +1 -1

@@ -1443,7 +1443,7 @@ defmodule OpenAgentsWeb.UI do

1443 1443
  attr :size, :atom, values: [:default, :xs, :sm, :lg], default: :default
1444 1444
1445 1445
  attr :action, :string,
1446
    default: "/auth/github?github_tools=enabled",
1446
    default: "/auth/github",
1447 1447
    doc: "where the sign-in posts; the caller owns the route"
1448 1448
1449 1449
  attr :class, :any, default: nil
lib/openagents_web/controllers/auth_controller.ex modified +1 -3

@@ -6,7 +6,7 @@ defmodule OpenAgentsWeb.AuthController do

6 6
  @attempt_session_key "github_oauth_attempt"
7 7
  @identity_session_key "posthog_identity"
8 8
9
  def start(conn, %{"github_tools" => "enabled"}) do
9
  def start(conn, _params) do
10 10
    case GitHubOAuth.begin_authorization() do
11 11
      {:ok, attempt, authorization_url} ->
12 12
        Analytics.capture("auth_started", Analytics.browser_distinct_id(conn))

@@ -21,8 +21,6 @@ defmodule OpenAgentsWeb.AuthController do

21 21
    end
22 22
  end
23 23
24
  def start(conn, _params), do: auth_failure(conn, "consent_required")
25
26 24
  def callback(conn, %{"code" => code, "state" => state}) do
27 25
    attempt = get_session(conn, @attempt_session_key)
28 26
    verifier = if is_map(attempt), do: attempt["verifier"]
lib/openagents_web/route_authority.ex modified +2 -1

@@ -9,6 +9,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

9 9
10 10
  @classes [
11 11
    :public_read,
12
    :public_action,
12 13
    :authenticated_browser,
13 14
    :authenticated_api,
14 15
    :operator,

@@ -191,7 +192,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

191 192
         )
192 193
193 194
  defp policy(%{path: "/auth/github", verb: :post}),
194
    do: declaration(:authenticated_browser, "explicit OAuth applicant", "identity:connect", true)
195
    do: declaration(:public_action, "OAuth applicant", "identity:sign-in", true)
195 196
196 197
  defp policy(%{path: "/auth/github/callback"}),
197 198
    do: declaration(:authenticated_browser, "one-time OAuth attempt", "identity:connect", true)
ops/deploy/fleet-startup.template.sh modified +1 -2

@@ -59,7 +59,7 @@ export AI_GATEWAY_API_KEY="$(secret openagents-vercel-gateway-api-key)"

59 59
60 60
export DATABASE_URL="ecto://sarah_app:${DB_PASSWORD}@127.0.0.1:5432/sarah"
61 61
export DNS_CLUSTER_QUERY="sarah.fleet.internal"
62
export GITHUB_OAUTH_SCOPES="repo,read:org"
62
export GITHUB_OAUTH_SCOPES="user:email"
63 63
export GITHUB_REDIRECT_URI="https://openagents.com/auth/github/callback"
64 64
export GITHUB_TOKEN_DECRYPTION_KEYS_JSON="{}"
65 65
export GITHUB_TOKEN_ENCRYPTION_KEY_ID="production-legacy-v1"

@@ -345,4 +345,3 @@ esac

345 345
346 346
347 347
348
ops/staging/gate-5-profile.sh modified +1 -1

@@ -15,7 +15,7 @@ set -eu

15 15
: "${OPENAI_API_KEY:?OPENAI_API_KEY is required}"
16 16
: "${SECRET_KEY_BASE:?SECRET_KEY_BASE is required}"
17 17
18
export GITHUB_OAUTH_SCOPES="repo,read:org"
18
export GITHUB_OAUTH_SCOPES="user:email"
19 19
export GITHUB_REDIRECT_URI="https://staging.openagents.com/auth/github/callback"
20 20
# Written as an explicit default rather than ${VAR:-{}}: POSIX expansion ends
21 21
# at the first `}`, so a preset value came back corrupted with a trailing
priv/docs/changelog.md modified +3 -3

@@ -1,10 +1,10 @@

1 1
# Changelog
2 2
3
## Coder 0.1
3
## Coder v0.1.0
4 4
5
Released August 2026.
5
Released August 26, 2026.
6 6
7
Coder 0.1 brings interactive coding, model access, tools, delegation, and
7
Coder v0.1.0 brings interactive coding, model access, tools, delegation, and
8 8
durable session history into the OpenAgents CLI.
9 9
10 10
### Work in your terminal
test/openagents/accounts_test.exs modified +8 -1

@@ -77,7 +77,7 @@ defmodule OpenAgents.AccountsTest do

77 77
    assert {:ok, user} = Accounts.upsert_github_user(profile(501, "token-owner"))
78 78
79 79
    assert {:error, :invalid_token_scopes} =
80
             Accounts.store_github_token(user, "gho_too_broad", ["read:user", "repo"])
80
             Accounts.store_github_token(user, "gho_too_broad", ["user:email", "repo"])
81 81
82 82
    assert {:ok, connected} = Accounts.store_github_token(user, "gho_retained")
83 83
    assert connected.github_token_key_id == "test-2026-08"

@@ -93,6 +93,13 @@ defmodule OpenAgents.AccountsTest do

93 93
             end)
94 94
95 95
    assert disconnected.id == user.id
96
97
    assert {:ok, repository_user} =
98
             Accounts.upsert_github_user(profile(502, "repository-token-owner"))
99
100
    assert {:ok, _repository_grant} =
101
             Accounts.store_github_token(repository_user, "gho_repository", ["repo", "read:org"])
102
96 103
    assert disconnected.github_token_ciphertext == nil
97 104
    assert disconnected.github_token_scopes == []
98 105
    assert {:error, :github_token_missing} = Accounts.github_token(disconnected)
test/openagents/github_oauth_test.exs modified +9 -9

@@ -7,7 +7,7 @@ defmodule OpenAgents.GitHubOAuthTest do

7 7
8 8
  setup do
9 9
    original_scopes = Application.fetch_env!(:openagents, :github_oauth_scopes)
10
    Application.put_env(:openagents, :github_oauth_scopes, ["repo", "read:org"])
10
    Application.put_env(:openagents, :github_oauth_scopes, ["user:email"])
11 11
12 12
    on_exit(fn ->
13 13
      Application.put_env(:openagents, :github_oauth_scopes, original_scopes)

@@ -22,7 +22,7 @@ defmodule OpenAgents.GitHubOAuthTest do

22 22
23 23
    assert query["client_id"] == "test-github-client-id"
24 24
    assert query["redirect_uri"] == "http://127.0.0.1:4002/auth/github/callback"
25
    assert query["scope"] == "repo read:org"
25
    assert query["scope"] == "user:email"
26 26
    assert query["state"] == attempt["state"]
27 27
    assert query["code_challenge_method"] == "S256"
28 28
    assert byte_size(query["code_challenge"]) == 43

@@ -65,7 +65,7 @@ defmodule OpenAgents.GitHubOAuthTest do

65 65
      Req.Test.json(conn, %{
66 66
        "access_token" => "short-lived-token",
67 67
        "token_type" => "bearer",
68
        "scope" => "read:org,repo"
68
        "scope" => "user:email"
69 69
      })
70 70
    end)
71 71

@@ -84,7 +84,7 @@ defmodule OpenAgents.GitHubOAuthTest do

84 84
85 85
    verifier = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)
86 86
87
    assert {:ok, profile, access_token, ["repo", "read:org"]} =
87
    assert {:ok, profile, access_token, ["user:email"]} =
88 88
             GitHubOAuth.exchange_and_fetch("github-code", verifier)
89 89
90 90
    assert access_token == "short-lived-token"

@@ -107,7 +107,7 @@ defmodule OpenAgents.GitHubOAuthTest do

107 107
      Req.Test.expect(__MODULE__, fn conn ->
108 108
        Req.Test.json(conn, %{
109 109
          "access_token" => "short-lived-token",
110
          "scope" => "repo read:org"
110
          "scope" => "user:email"
111 111
        })
112 112
      end)
113 113

@@ -140,7 +140,7 @@ defmodule OpenAgents.GitHubOAuthTest do

140 140
    Req.Test.expect(__MODULE__, fn conn ->
141 141
      Req.Test.json(conn, %{
142 142
        "access_token" => "provider-token",
143
        "scope" => "repo,read:org"
143
        "scope" => "user:email"
144 144
      })
145 145
    end)
146 146

@@ -157,7 +157,7 @@ defmodule OpenAgents.GitHubOAuthTest do

157 157
  end
158 158
159 159
  test "a missing or broadened granted scope fails before profile lookup" do
160
    for scope <- [nil, "", "repo", "read:org", "read:user", "repo,read:org,admin:org"] do
160
    for scope <- [nil, "", "repo", "read:org", "read:user", "user:email,repo"] do
161 161
      setup_req_test()
162 162
163 163
      Req.Test.expect(__MODULE__, fn conn ->

@@ -172,9 +172,9 @@ defmodule OpenAgents.GitHubOAuthTest do

172 172
    end
173 173
  end
174 174
175
  test "the retained grant model requires exactly repository and organization read scopes" do
175
  test "sign-in scope stays separate from repository authorization scopes" do
176 176
    assert GitHubOAuth.required_scopes() == ["repo", "read:org"]
177
    assert GitHubOAuth.requested_scopes() == ["repo", "read:org"]
177
    assert GitHubOAuth.requested_scopes() == ["user:email"]
178 178
  end
179 179
180 180
  defp setup_req_test do
test/openagents/runtime_config_test.exs modified +9

@@ -45,6 +45,15 @@ defmodule OpenAgents.RuntimeConfigTest do

45 45
    refute encoded =~ "ecto://"
46 46
  end
47 47
48
  test "GitHub sign-in accepts only the email scope" do
49
    assert {:error, %{setting: :github_oauth_scopes}} =
50
             staging_settings()
51
             |> Map.put(:github_oauth_scopes, ["repo", "read:org"])
52
             |> RuntimeConfig.validate()
53
54
    assert {:ok, _config} = RuntimeConfig.validate(staging_settings())
55
  end
56
48 57
  test "staging gates refuse features before their admission gate" do
49 58
    settings = staging_settings() |> put_nested(:voice, :enabled, true)
50 59
test/openagents_web/auth_controller_test.exs modified +7 -7

@@ -43,7 +43,7 @@ defmodule OpenAgentsWeb.AuthControllerTest do

43 43
    refute user.github_token_ciphertext =~ "ephemeral-github-token"
44 44
    assert {:ok, "ephemeral-github-token"} = Accounts.github_token(user)
45 45
    assert user.github_token_key_id == "test-2026-08"
46
    assert user.github_token_scopes == ["repo", "read:org"]
46
    assert user.github_token_scopes == ["user:email"]
47 47
    assert user.github_token_connected_at
48 48
49 49
    cookie = authenticated |> get_resp_header("set-cookie") |> Enum.join(";")

@@ -99,15 +99,15 @@ defmodule OpenAgentsWeb.AuthControllerTest do

99 99
    assert get_session(callback, "user_id") == nil
100 100
  end
101 101
102
  test "GitHub tools require an explicit retained-token choice", %{conn: conn} do
103
    refused =
102
  test "GitHub sign-in does not require repository-tool consent", %{conn: conn} do
103
    started =
104 104
      conn
105 105
      |> init_test_session(%{})
106 106
      |> put_req_header("x-csrf-token", Plug.CSRFProtection.get_csrf_token())
107 107
      |> post(~p"/auth/github")
108 108
109
    assert redirected_to(refused) == ~p"/?auth_error=consent_required"
110
    assert get_session(refused, "github_oauth_attempt") == nil
109
    assert redirected_to(started) =~ "https://github.com/login/oauth/authorize?"
110
    assert get_session(started, "github_oauth_attempt")
111 111
  end
112 112
113 113
  test "disconnect revokes the GitHub grant and clears local token metadata", %{conn: conn} do

@@ -203,7 +203,7 @@ defmodule OpenAgentsWeb.AuthControllerTest do

203 203
    conn
204 204
    |> init_test_session(%{})
205 205
    |> put_req_header("x-csrf-token", csrf_token)
206
    |> post(~p"/auth/github?github_tools=enabled")
206
    |> post(~p"/auth/github")
207 207
  end
208 208
209 209
  defp attempt_and_state(conn) do

@@ -218,7 +218,7 @@ defmodule OpenAgentsWeb.AuthControllerTest do

218 218
    Req.Test.expect(__MODULE__, fn conn ->
219 219
      Req.Test.json(conn, %{
220 220
        "access_token" => "ephemeral-github-token",
221
        "scope" => "repo,read:org"
221
        "scope" => "user:email"
222 222
      })
223 223
    end)
224 224
test/openagents_web/authenticated_route_gate_test.exs modified +4 -3

@@ -18,9 +18,10 @@ defmodule OpenAgentsWeb.AuthenticatedRouteGateTest do

18 18
  refuse: a redirect to the public root, or `401` for the session-authenticated
19 19
  API routes that answer a client rather than a browser.
20 20
21
  The two GitHub OAuth entries are in this class and refuse the same way. They
22
  are where a person authenticates, so an anonymous request reaches them, but
23
  it leaves with an `auth_error` and no session rather than with a page.
21
  The GitHub callback is in this class and refuses an anonymous request that
22
  does not carry a valid one-time OAuth attempt. The sign-in action is the
23
  bounded public action that creates that attempt, so it is classified
24
  separately.
24 25
25 26
  The websocket at `/live` is classified in the same class and is not
26 27
  dispatchable here; `OpenAgentsWeb.UserAuth.on_mount/4` gates it and
test/openagents_web/controllers/data_controller_test.exs modified +1 -1

@@ -134,7 +134,7 @@ defmodule OpenAgentsWeb.DataControllerTest do

134 134
             "credential_exported" => false,
135 135
             "product_data_deletion" => "retained_until_explicit_disconnect",
136 136
             "rotated_at" => nil,
137
             "scopes" => ["repo", "read:org"]
137
             "scopes" => ["user:email"]
138 138
           }
139 139
140 140
    refute inspect(export) =~ "gho_export_sentinel"
test/openagents_web/controllers/repository_import_controller_test.exs modified +12 -8

@@ -1,7 +1,7 @@

1 1
defmodule OpenAgentsWeb.RepositoryImportControllerTest do
2 2
  use OpenAgentsWeb.ConnCase, async: false
3 3
4
  alias OpenAgents.{Accounts, ApiTokens, Repo}
4
  alias OpenAgents.{Accounts, ApiTokens, GitHubOAuth, Repo}
5 5
6 6
  setup {Req.Test, :verify_on_exit!}
7 7

@@ -19,7 +19,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

19 19
20 20
  test "POST /api/v1/user/repos/imports accepts one frozen GitHub snapshot", %{conn: conn} do
21 21
    user = github_user("repository-import-api", "octavia")
22
    assert {:ok, user} = Accounts.store_github_token(user, "gho_import_fixture")
22
    assert {:ok, user} = store_repository_grant(user, "gho_import_fixture")
23 23
    main_sha = String.duplicate("a", 40)
24 24
    tag_sha = String.duplicate("b", 40)
25 25

@@ -74,7 +74,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

74 74
75 75
  test "an import inherits the GitHub repository visibility when omitted", %{conn: conn} do
76 76
    user = github_user("repository-public-import-api", "octavia")
77
    assert {:ok, user} = Accounts.store_github_token(user, "gho_public_import_fixture")
77
    assert {:ok, user} = store_repository_grant(user, "gho_public_import_fixture")
78 78
    main_sha = String.duplicate("c", 40)
79 79
80 80
    expect_import_source(user, main_sha, nil, false)

@@ -92,7 +92,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

92 92
93 93
  test "organization creation requires an active GitHub administrator membership", %{conn: conn} do
94 94
    user = github_user("repository-org-api")
95
    assert {:ok, user} = Accounts.store_github_token(user, "gho_org_fixture")
95
    assert {:ok, user} = store_repository_grant(user, "gho_org_fixture")
96 96
97 97
    Req.Test.expect(__MODULE__, fn github_conn ->
98 98
      assert github_conn.request_path == "/user/memberships/orgs"

@@ -129,7 +129,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

129 129
      conn: conn
130 130
    } do
131 131
      user = github_user("repository-foreign-import", "octavia")
132
      assert {:ok, user} = Accounts.store_github_token(user, "gho_foreign_fixture")
132
      assert {:ok, user} = store_repository_grant(user, "gho_foreign_fixture")
133 133
134 134
      expect_foreign_source(String.duplicate("d", 40), false, "MIT")
135 135

@@ -156,7 +156,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

156 156
      conn: conn
157 157
    } do
158 158
      user = github_user("repository-mirror-api", "octavia")
159
      assert {:ok, user} = Accounts.store_github_token(user, "gho_mirror_fixture")
159
      assert {:ok, user} = store_repository_grant(user, "gho_mirror_fixture")
160 160
      main_sha = String.duplicate("e", 40)
161 161
162 162
      expect_foreign_source(main_sha, false, "MIT")

@@ -191,7 +191,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

191 191
192 192
    test "an upstream with no license records the absence rather than omitting it", %{conn: conn} do
193 193
      user = github_user("repository-unlicensed-mirror", "octavia")
194
      assert {:ok, user} = Accounts.store_github_token(user, "gho_unlicensed_fixture")
194
      assert {:ok, user} = store_repository_grant(user, "gho_unlicensed_fixture")
195 195
196 196
      expect_foreign_source(String.duplicate("f", 40), false, nil)
197 197

@@ -209,7 +209,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

209 209
210 210
    test "a private source cannot be mirrored", %{conn: conn} do
211 211
      user = github_user("repository-private-mirror", "octavia")
212
      assert {:ok, user} = Accounts.store_github_token(user, "gho_private_mirror_fixture")
212
      assert {:ok, user} = store_repository_grant(user, "gho_private_mirror_fixture")
213 213
214 214
      expect_foreign_source(String.duplicate("1", 40), true, "MIT")
215 215

@@ -238,6 +238,10 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

238 238
    end
239 239
  end
240 240
241
  defp store_repository_grant(user, token) do
242
    Accounts.store_github_token(user, token, GitHubOAuth.required_scopes())
243
  end
244
241 245
  defp expect_foreign_source(main_sha, private?, license) do
242 246
    Req.Test.expect(__MODULE__, fn github_conn ->
243 247
      assert github_conn.request_path == "/repos/tobi/walgit"
test/openagents_web/device_sign_in_return_test.exs modified +4 -4

@@ -68,7 +68,7 @@ defmodule OpenAgentsWeb.DeviceSignInReturnTest do

68 68
      landing
69 69
      |> recycle()
70 70
      |> put_req_header("x-csrf-token", Plug.CSRFProtection.get_csrf_token())
71
      |> post(~p"/auth/github?github_tools=enabled")
71
      |> post(~p"/auth/github")
72 72
73 73
    state = oauth_state(started)
74 74
    expect_github(4_129, "device-return-person")

@@ -109,7 +109,7 @@ defmodule OpenAgentsWeb.DeviceSignInReturnTest do

109 109
      conn
110 110
      |> init_test_session(%{})
111 111
      |> put_req_header("x-csrf-token", Plug.CSRFProtection.get_csrf_token())
112
      |> post(~p"/auth/github?github_tools=enabled")
112
      |> post(~p"/auth/github")
113 113
114 114
    state = oauth_state(started)
115 115
    expect_github(4_130, "ordinary-sign-in-person")

@@ -174,7 +174,7 @@ defmodule OpenAgentsWeb.DeviceSignInReturnTest do

174 174
        bounced
175 175
        |> recycle()
176 176
        |> put_req_header("x-csrf-token", Plug.CSRFProtection.get_csrf_token())
177
        |> post(~p"/auth/github?github_tools=enabled")
177
        |> post(~p"/auth/github")
178 178
179 179
      state = oauth_state(started)
180 180
      expect_github(4_131, "crafted-code-person")

@@ -231,7 +231,7 @@ defmodule OpenAgentsWeb.DeviceSignInReturnTest do

231 231
    Req.Test.expect(__MODULE__, fn conn ->
232 232
      Req.Test.json(conn, %{
233 233
        "access_token" => "ephemeral-github-token",
234
        "scope" => "repo,read:org"
234
        "scope" => "user:email"
235 235
      })
236 236
    end)
237 237
test/openagents_web/home_controller_test.exs modified +1 -1

@@ -12,7 +12,7 @@ defmodule OpenAgentsWeb.HomeControllerTest do

12 12
    # current product identity.
13 13
    assert html =~ "Introducing"
14 14
    assert html =~ "Your all-in-one coding agent."
15
    assert html =~ ~s(action="/auth/github?github_tools=enabled")
15
    assert html =~ ~s(action="/auth/github")
16 16
    assert html =~ "Log in with GitHub"
17 17
18 18
    # The scope disclosure was removed from the hero at the owner's direction.
test/openagents_web/live/changelog_live_test.exs modified +3 -2

@@ -32,10 +32,11 @@ defmodule OpenAgentsWeb.ChangelogLiveTest do

32 32
    assert redirected_to(conn, 302) == ~p"/docs/changelog"
33 33
  end
34 34
35
  test "the documentation changelog presents Coder 0.1", %{conn: conn} do
35
  test "the documentation changelog presents Coder v0.1.0 with its release date", %{conn: conn} do
36 36
    {:ok, _view, html} = live(conn, ~p"/docs/changelog")
37 37
38
    assert html =~ "Coder 0.1"
38
    assert html =~ "Coder v0.1.0"
39
    assert html =~ "Released August 26, 2026."
39 40
    assert html =~ "Choose how inference runs"
40 41
    assert html =~ "Delegate work"
41 42
  end
test/openagents_web/live/repository_live_test.exs modified +16 -2

@@ -250,7 +250,14 @@ defmodule OpenAgentsWeb.RepositoryLiveTest do

250 250
251 251
  test "one-time import picker creates an immutable GitHub import receipt", %{conn: conn} do
252 252
    user = github_user("repository-live-import", "import-owner")
253
    assert {:ok, user} = Accounts.store_github_token(user, "gho_live_import")
253
254
    assert {:ok, user} =
255
             Accounts.store_github_token(
256
               user,
257
               "gho_live_import",
258
               OpenAgents.GitHubOAuth.required_scopes()
259
             )
260
254 261
    main_sha = String.duplicate("a", 40)
255 262
256 263
    Req.Test.stub(__MODULE__, &github_import_response(&1, user, main_sha))

@@ -283,7 +290,14 @@ defmodule OpenAgentsWeb.RepositoryLiveTest do

283 290
284 291
  test "one-time import picker defaults to the GitHub repository visibility", %{conn: conn} do
285 292
    user = github_user("repository-live-public-import", "import-owner")
286
    assert {:ok, user} = Accounts.store_github_token(user, "gho_live_public_import")
293
294
    assert {:ok, user} =
295
             Accounts.store_github_token(
296
               user,
297
               "gho_live_public_import",
298
               OpenAgents.GitHubOAuth.required_scopes()
299
             )
300
287 301
    main_sha = String.duplicate("c", 40)
288 302
289 303
    Req.Test.stub(__MODULE__, fn github_conn ->
test/openagents_web/route_authority_test.exs modified +9

@@ -23,6 +23,15 @@ defmodule OpenAgentsWeb.RouteAuthorityTest do

23 23
    end)
24 24
  end
25 25
26
  test "GitHub sign-in is the bounded public identity action" do
27
    route = route!(:post, "/auth/github")
28
29
    assert route.class == :public_action
30
    assert route.principal == "OAuth applicant"
31
    assert route.scope == "identity:sign-in"
32
    assert route.mutation
33
  end
34
26 35
  test "public forge reads and bearer-authenticated forge writes are separate" do
27 36
    read = route!(:get, "/api/v1/repos/:owner/:repo/issues")
28 37
    write = route!(:post, "/api/v1/repos/:owner/:repo/issues")

This page updates live while a promote is in flight · changelog