Skip to content

chore: sync fork with upstream (ColeMurray/background-agents)#2

Open
jasoncuriano wants to merge 13 commits into
mainfrom
sync-upstream-8efc9e6
Open

chore: sync fork with upstream (ColeMurray/background-agents)#2
jasoncuriano wants to merge 13 commits into
mainfrom
sync-upstream-8efc9e6

Conversation

@jasoncuriano

Copy link
Copy Markdown
Collaborator

Syncs this fork's main with ColeMurray/background-agents:main (13 commits, fast-forward, no conflicts).

Includes: custom domain support for web, GitLab full namespace path fix, several type-safety boundary validation fixes, D1 duplicate migration version guard, Linear OAuth failure classification, and a few smaller fixes.

ColeMurray and others added 13 commits July 1, 2026 09:36
## Summary
- Document Slack's separate Select Menus Options Load URL for external
select menus
- Point the Options Load URL at the existing `/interactions` endpoint
- Clarify that searchable repository pickers require this configuration

## Why
Slack quick-pick buttons are rendered inline, but the searchable repo
dropdown uses an `external_select`. Slack only requests those dynamic
options when the app has a Select Menus Options Load URL configured, so
the dropdown can show `no result` even though repos are available.

## Validation
- `npx prettier --check docs/GETTING_STARTED.md`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated the Slack setup guide to add a required **Select Menus →
Options Load URL** configuration for searchable repository pickers.
* Clarified that the Options Load URL should match the same
`/interactions` endpoint used for the Request URL.
* Renumbered/reformatted the subsequent setup steps to reflect the added
configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Add an explicit `No repository` option to the homepage session target
selector.
- Create repo-less sessions from the web composer with `repoOwner: null`
and `repoName: null`.
- Hide branch selection and omit branch context for repo-less sessions.
- Default to `No repository` when the user has no accessible
repositories.

## Testing
- `npm test -w @open-inspect/web -- src/app/(app)/page.test.tsx`
- `npm run typecheck -w @open-inspect/web`
- `npm run lint -w @open-inspect/web`
- `npm test -w @open-inspect/web`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an explicit “start without a repository” option for session
creation.
* Updated the repository picker to reflect this choice and only show
branch selection when a repository is selected.
* **Bug Fixes**
* Improved session startup when no repositories are available, including
clearer guidance in the empty state.
* **Tests**
* Added UI flow coverage to ensure sessions can start with a “no
repository” target and send the expected session creation payload.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation. It replaces
selected high-risk TypeScript assertions at OAuth/API trust boundaries
with Zod `safeParse` validation, following the TypeScript Coding
Standards unsafe-cast / parse-don't-assert guidance and the Zod
boundary-validation pattern established in PR ColeMurray#807.

| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/control-plane/src/auth/github.ts:48` | HIGH | `(await
response.json()) as GitHubTokenResponse & { error?: string;
error_description?: string }` for GitHub OAuth code exchange | Added
`githubTokenResponseSchema` as the `GitHubTokenResponse` source of truth
and parse through `parseGitHubTokenResponse` with preserved OAuth error
handling |
| `packages/control-plane/src/auth/github.ts:81` | HIGH | `(await
response.json()) as GitHubTokenResponse & { error?: string;
error_description?: string }` for GitHub OAuth refresh | Reused
`githubTokenResponseSchema.safeParse` at the refresh boundary |
| `packages/control-plane/src/auth/openai.ts:50` | HIGH |
`response.json() as Promise<OpenAITokenResponse>` for OpenAI OAuth
refresh | Replaced the hand-written interface with
`openAITokenResponseSchema` / `z.infer` and validate the parsed response
before returning |

Verification:

| Command | Result |
| --- | --- |
| `npm test -w @open-inspect/control-plane -- src/auth/github.test.ts
src/auth/openai.test.ts` | Passed |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run build -w @open-inspect/control-plane` | Passed |
| `npm run typecheck` | Passed |
| `npm run lint` | Passed after temporarily moving the pre-existing
untracked `.opencode/` tooling directory out of the workspace so the
root lint command evaluated tracked repository files |
| `npm run format` | Passed |
| `npm test -w @open-inspect/control-plane` | Passed |

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/c0eee7014d656c67ad338b45f014cd85)*

Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
This is an automated nightly unsafe-cast remediation. It replaces three
high-risk TypeScript assertions at request/internal-session/API trust
boundaries with Zod `safeParse` validation, following the TypeScript
Coding Standards unsafe-cast / parse-don't-assert guidance and the Zod
boundary-validation pattern established in PR ColeMurray#807.

| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/control-plane/src/routes/session-child-spawn.ts:36` | HIGH |
`(await request.json()) as SpawnChildSessionRequest` for child session
creation input | Added shared `spawnChildSessionRequestSchema` with
`z.infer` as the type source and validate the request body with
`safeParse`, preserving the existing 400 `title and prompt are required`
path |
| `packages/control-plane/src/routes/session-child-spawn.ts:82` | HIGH |
`(await spawnContextRes.json()) as SpawnContext` for parent session
state used to initialize child sessions | Added shared
`spawnContextSchema` with nullable fields modeled explicitly and
validate the internal response with `safeParse`, preserving the existing
500 `Failed to get parent session context` path |
| `packages/github-bot/src/github-auth.ts:132` | HIGH | `(await
response.json()) as { permission: string }` for GitHub collaborator
permission authorization response | Added a local Zod response schema
and validate with `safeParse`, preserving `{ hasPermission: false,
error: true }` on malformed API responses |

Verification:

| Command | Result |
| --- | --- |
| `npm test -w @open-inspect/shared --
src/types/boundary-schemas.test.ts` | Passed |
| `npm test -w @open-inspect/github-bot -- test/github-auth.test.ts` |
Passed |
| `npm test -w @open-inspect/control-plane --
src/router.spawn-child.test.ts` | Passed after `@open-inspect/shared`
rebuild refreshed the new exports |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run build -w @open-inspect/control-plane` | Passed |
| `npm run build -w @open-inspect/github-bot` | Passed |
| `npm test -w @open-inspect/shared` | Passed |
| `npm test -w @open-inspect/control-plane` | Passed |
| `npm test -w @open-inspect/github-bot` | Passed |
| `npm run typecheck` | Passed |
| `npm run lint` | Passed after temporarily moving the pre-existing
untracked `.opencode/` tooling directory out of the workspace so the
root lint command evaluated tracked repository files |
| `npm run format` | Passed |
| `git diff --check` | Passed |

References: TypeScript Coding Standards unsafe-cast / parse-don't-assert
guidance; Zod boundary-validation pattern established in PR ColeMurray#807.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/e71a3545b1132e0bd0e5132142bf3fc3)*

Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces
selected high-risk response-boundary assertions with Zod validation
following the TypeScript Coding Standards unsafe-cast /
parse-don't-assert guidance and the Zod boundary-validation pattern
established in PR ColeMurray#807. This PR is marked DRAFT in the title/body
because two exact verification gates are blocked in this workspace, as
detailed below.

Requested label: `automation:unsafe-cast`

## Findings Fixed
| file:line | risk | cast removed | how it was fixed |
| --- | --- | --- | --- |
| `packages/web/src/lib/auth.ts:77` | HIGH | `(await response.json()) as
GithubEmail[]` from GitHub's `/user/emails` API, feeding auth allowlist
email resolution | Package-local Zod schema in `github-email-schema.ts`;
validates consumed fields and accepts `visibility: null`; malformed
responses fail closed with `[]` |
| `packages/github-bot/src/handlers.ts:69` | HIGH | `(await
response.json()) as { sessionId: string }`, feeding GitHub bot session
state | Package-local Zod schema in `control-plane-responses.ts`;
invalid success responses throw through the existing session creation
error path |
| `packages/github-bot/src/handlers.ts:91` | HIGH | `(await
response.json()) as { messageId: string }`, feeding GitHub bot message
state | Package-local Zod schema in `control-plane-responses.ts`;
invalid success responses throw through the existing prompt delivery
error path |

## Verification
| command | result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | PASS |
| `npm run build -w @open-inspect/github-bot` | PASS |
| `npm run build -w @open-inspect/web` | FAIL: TypeScript passes, then
prerender of `/automations/new` fails with `TypeError: Cannot read
properties of null (reading 'useContext')`; this page was not touched by
this change |
| `npm run typecheck` | PASS |
| `npm run lint` | FAIL: root lint scans pre-existing untracked
`.opencode/` tooling files and reports globals such as `process`,
`fetch`, `Headers`; committed files are clean |
| `npm run lint -- --ignore-pattern '.opencode/**'` | PASS |
| `npm run lint -w @open-inspect/shared` | PASS |
| `npm run lint -w @open-inspect/github-bot` | PASS |
| `npm run lint -w @open-inspect/web` | PASS |
| `npm test -w @open-inspect/shared` | PASS |
| `npm test -w @open-inspect/github-bot` | PASS |
| `npm test -w @open-inspect/web` | PASS |

## Dependency Check
Added `zod@^4.4.3` to `@open-inspect/web` for a genuinely package-local
auth boundary. `package-lock.json` changed only to add the corresponding
`packages/web` dependency entry; no unrelated lockfile churn remains.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/662960e2a119deb77df0849505476f36)*

Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary
- remove the exact `total` count from the sessions list API
- derive `hasMore` by fetching one extra session row
- update archived-session pagination to use the returned `hasMore` flag

## Why
The frontend only consumes `sessions` and `hasMore`, so the D1
`COUNT(*)` query was doing unnecessary work on a hot session-list path.
Removing it avoids the growing table scan for `status != archived`
requests.

## Validation
- `npm test -w @open-inspect/control-plane -- session-index`
- `npm test -w @open-inspect/web -- session-list control-plane-query
data-controls-settings session-sidebar`
- `npm run test:integration -w @open-inspect/control-plane --
d1-session-index auth`
- `npm run typecheck`
- `npm run lint -w @open-inspect/control-plane`
- `npm run lint -w @open-inspect/web`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Session lists now use server-provided pagination info, making “Load
more” behavior more reliable.
* Archived sessions now load more accurately without relying on page
size checks.

* **Improvements**
* Session list responses no longer include a total count and now return
a clearer `hasMore` indicator.
* Pagination handling was updated across the app to match the new
session list response format.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…y#854)

## Problem

`scripts/d1-migrate.sh` tracks applied migrations by their **numeric
prefix** — `VERSION=$(echo "$FILENAME" | grep -oE '^[0-9]+')`, stored as
the `_schema_migrations.version` primary key, and the skip check is
`grep -qxF "$VERSION"`.

If two migration files ever share a prefix, only one is recorded and the
other is **silently skipped — permanently**. A re-run never recovers it,
because the number is already marked applied. The result is schema drift
that doesn't surface at migrate time; it shows up later as a confusing
runtime error (e.g. `table X has no column named Y`).

This is easy to hit without anyone doing anything wrong: two PRs in
flight each grab the next sequential number (`00NN_...`), both pass CI
independently, and after both merge the directory has two `00NN_*.sql`
files. One migration is then dropped on the next deploy.

## Fix

Detect colliding numeric prefixes at the start of the run and fail with
a clear, actionable message instead of silently skipping:

```
ERROR: duplicate migration version prefixes detected:
  0022
Renumber the colliding files so each prefix is unique before deploying.
```

No behavior change when prefixes are unique. One-file change, no new
dependencies (`sort | uniq -d` over the basenames' prefixes).

## Why a guard rather than reworking the tracking

Keying on the full filename instead of the prefix would also avoid the
silent skip, but it changes the `_schema_migrations` PK semantics and
the INSERT path for existing deployments. The guard is the minimal,
behavior-preserving fix; it just turns a silent data-loss case into a
loud, self-explaining failure.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Added a pre-flight validation for migration files to stop the
migration process when two SQL files share the same version prefix.
* Migrations now fail fast with clear error output when duplicate
migration numbers are detected (and require filenames with numeric
prefixes).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This is an automated nightly unsafe-cast remediation sweep. It replaces
selected scheduler boundary assertions with Zod parsing so untrusted
internal request bodies are validated before they drive automation
matching, run creation, or run completion state. This follows the
TypeScript Coding Standards guidance for unsafe casts /
parse-don't-assert and the Zod boundary-validation pattern established
in PR ColeMurray#807.

| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/control-plane/src/scheduler/durable-object.ts:427` | HIGH |
`(await request.json()) as AutomationEvent` for scheduler automation
events feeding matching, dedup, and run creation | Shared
`automationEventSchema` Zod discriminated union in
`@open-inspect/shared`, parsed with `safeParse` and 400 on invalid
payload |
| `packages/control-plane/src/scheduler/durable-object.ts:598` | HIGH |
`(await request.json()) as { automationId: string }` for manual trigger
run creation | Package-local Zod `manualTriggerBodySchema`, parsed with
`safeParse` and existing 400 `automationId required` response |
| `packages/control-plane/src/scheduler/durable-object.ts:686` | HIGH |
`(await request.json()) as { automationId; runId; sessionId; messageId?;
success; error? }` for run-complete state transitions | Package-local
Zod `runCompleteBodySchema`, parsed with `safeParse` and 400 on invalid
callback |

Verification commands run in a clean worktree for this branch:

| Command | Result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | Passed |
| `npm run build -w @open-inspect/control-plane` | Passed |
| `npm run typecheck` | Passed |
| `npm run lint` | Passed |
| `npm run format` | Passed |
| `npm test -w @open-inspect/shared` | Passed, 22 files / 275 tests |
| `npm test -w @open-inspect/control-plane` | Passed, 89 files / 1415
tests |

Note: in the original workspace, `npm run lint` was also attempted and
failed only because of a pre-existing untracked `.opencode/` directory
outside this PR; the exact command passed in the clean worktree
containing only committed branch files.

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/e7ba177d5123895cea9e49c69689c0e1)*

---------

Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
This is an automated nightly unsafe-cast remediation sweep. It replaces
selected high-risk TypeScript boundary assertions with
parse-don't-assert validation, following the TypeScript Coding Standards
for unsafe casts and the Zod boundary-validation pattern established in
PR ColeMurray#807.

| Finding | Risk | Cast removed | Fix |
| --- | --- | --- | --- |
| `packages/web/src/hooks/use-session-socket.ts:139` | HIGH | WebSocket
message `raw as WsMessage` after only checking for a `type` key | Added
shared `serverMessageSchema` in `@open-inspect/shared` and now
`safeParse`s WebSocket messages before handling them. |
| `packages/web/src/app/api/sessions/[id]/title/route.ts:27` | HIGH |
`await request.json() as { title?: string }` on an API request body |
Added a minimal inline parser for the request body that preserves the
existing 400 invalid-body path. |

## Verification

| Command | Result |
| --- | --- |
| `npm run build -w @open-inspect/shared` | Passed |
| `env -u NODE_ENV npm run build -w @open-inspect/web` | Passed; the
plain inherited shell environment had a non-standard `NODE_ENV` that
caused Next prerendering to fail before this retry. |
| `npm run typecheck` | Passed |
| `npm run lint` | Passed after temporarily moving the untracked local
`.opencode/` tool directory aside and restoring it; the tracked repo
files lint clean. |
| `npm run format` | Passed |
| `npm test -w @open-inspect/shared` | Passed: 21 files, 274 tests |
| `npm test -w @open-inspect/web` | Passed: 48 files, 513 tests |

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/bc3972ace378acf23238764498c05c1f)*

Co-authored-by: OpenInspect <open-inspect@noreply.github.com>
Co-authored-by: Cole Murray <colemurray.cs@gmail.com>
## Summary

Linear agent-session auth failures now preserve the concrete reason they
failed instead of collapsing every OAuth problem into a bare
missing-client return.

When a workspace token is missing, malformed, expired without a refresh
token, rejected by Linear during refresh, or blocked by a KV/read
failure, the token loader raises a `LinearAuthError` with the failure
reason and any available OAuth response details. Agent-session webhooks
catch that error, log `agent_session.no_oauth_token` with the org,
issue, trace ID, mode, and auth failure reason, then stop before
creating or prompting an Open-Inspect session.

The final behavior is intentionally log-only for these agent-session
paths. Earlier fallback issue-comment handling was removed after review
because a failed OAuth refresh usually means operators should inspect
the structured logs rather than trying to notify Linear through the
static API-key fallback.

## Validation

- npm run lint
- npm run typecheck
- npm test

## Post-Deploy Monitoring & Validation

- Watch Linear bot logs for `agent_session.no_oauth_token`,
`oauth.refresh_failed`, `oauth.refresh_error`, and
`oauth.token_read_error`.
- Healthy signal: auth failures include `auth_failure_reason`, issue
context, and trace ID, with no control-plane session or prompt created
after the auth failure.
- Failure signal: repeated `token_read_error` / `refresh_error` spikes,
missing issue context in `agent_session.no_oauth_token`, or unexpected
Linear agent-session requests continuing after auth failure.
- Validation window: first deploy after merge; owner: operator handling
the Linear bot deploy.

Related: ColeMurray#863

---

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
![GPT-5](https://img.shields.io/badge/GPT--5-000000)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of expired or invalid Linear OAuth tokens, reducing
failed agent session starts and follow-ups.
* When authentication can’t be restored, the system now skips the action
safely and records clearer failure details.
* Refresh failures are now classified more consistently, helping
diagnose issues like invalid grants and token read errors.

* **Tests**
* Added coverage for token expiry, refresh success/failure, and agent
session flows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Support a custom domain for the Cloudflare web Worker

Adds the ability to serve the web app on a custom domain (e.g.
`app.example.com`)
instead of the default `*.workers.dev` URL when `web_platform =
"cloudflare"`.

There was an existing `cloudflare_zone_id` terraform variable, but it
was unused.

### Changes

- **`variables.tf`** — new optional `cloudflare_custom_domain` variable
(the hostname to attach), alongside the existing `cloudflare_zone_id`.
Defaults to `null`.
- **`web-cloudflare.tf`** — new
`cloudflare_workers_custom_domain.web_app` resource that attaches the
hostname to the web Worker. Cloudflare provisions the DNS record and
edge certificate. Gated on `local.web_custom_domain_enabled` and depends
on the Worker deploy.
- **`locals.tf`** — added `web_custom_domain_enabled` flag and a third
case to `web_app_url`. When a custom domain is set, the app URL (used
for `NEXTAUTH_URL` and cross-service config) becomes
`https://<custom-domain>` instead of the `workers.dev` URL.
- **`terraform.tfvars.example`** — documented the new
`cloudflare_custom_domain` input under the Cloudflare zone section.

### Behavior

The custom domain only activates when **all** of the following are true:

- `web_platform = "cloudflare"`
- `cloudflare_zone_id` is set
- `cloudflare_custom_domain` is non-empty

Otherwise the deployment falls back to the existing `workers.dev` URL —
no change for existing deployments.

### Notes

- Requires a Cloudflare zone you control (set `cloudflare_zone_id`).
- The Cloudflare API token needs **Workers Routes: Edit** to manage the
custom domain.
- `terraform validate` passes cleanly (dropped the deprecated
`environment` attribute on the resource).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added optional Cloudflare custom-domain support for production web
workers, including automatic URL switching to the custom hostname when
configured.
* **Safety Checks**
* Added validation to prevent misconfigured custom-domain/zone settings;
falls back to the default workers subdomain when required inputs are
missing.
* **Documentation**
* Expanded production Cloudflare setup guidance and examples for the
custom-domain option.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#882)

Follow-ups to ColeMurray#747 (custom domain support for the Cloudflare web
Worker).

## Bug: `terraform plan` fails for every Cloudflare-platform deployment
without a custom domain

Terraform's `coalesce()` errors when **all** arguments are null or empty
strings — `coalesce(null, "")` is a hard error, not `""`:

```
Call to function "coalesce" failed: no non-null, non-empty-string arguments.
```

The `web_custom_domain_enabled` local from ColeMurray#747 evaluates
`trimspace(coalesce(var.cloudflare_custom_domain, ""))` whenever
`web_platform == "cloudflare"` (`&&` short-circuits, so Vercel
deployments escape). Both `cloudflare_custom_domain` and
`cloudflare_zone_id` default to `null`, so any existing `web_platform =
"cloudflare"` deployment that hasn't opted into a custom domain now
fails at plan time. Setting `cloudflare_custom_domain = ""` explicitly
errors the same way.

This slipped past `terraform validate` because validate treats variables
as unknown values — the error only fires on plan/apply with concrete
values.

**Fix**: null-safe normalization locals (`var.x == null ? "" :
trimspace(var.x)`) consumed by the gate, the host, and the
`cloudflare_workers_custom_domain` resource. This also fixes the
inconsistency where the gate trimmed the domain but the resource/URL
consumed the raw value.

## Hard validation instead of an advisory warning

The `check` block from ColeMurray#747 had the same `coalesce` problem (it would
report an evaluation error rather than its intended message), and as a
warning it let a misconfigured domain silently fall back to workers.dev
— leaving `NEXTAUTH_URL` and OAuth callbacks pointing somewhere the
operator didn't intend. Replaced with two hard failures:

- a `validation` block on `cloudflare_custom_domain` for hostname shape:
must be a bare hostname — rejects scheme, port, path, trailing dot,
whitespace, wildcards
- a `terraform_data.cloudflare_custom_domain_gate` precondition in
`checks.tf` for the cross-input policy (same pattern as the existing
`access_control_gate`), expressed against the normalized locals as
`local.web_custom_domain == "" || local.web_custom_domain_enabled`: a
set domain requires `web_platform = "cloudflare"` and a non-empty
`cloudflare_zone_id` (also catches the previously-silent
domain-set-on-Vercel case)

`null`, `""`, and whitespace-only all still mean "not configured".

## Single canonical origin

Attaching a custom domain doesn't disable the workers.dev route, so the
app was reachable on two origins with `NEXTAUTH_URL` pointing at only
one. The generated `wrangler.production.toml` now sets `workers_dev =
false` when the custom domain is enabled (`true` otherwise — existing
behavior, now explicit).

> **Note for deployers already running ColeMurray#747 with a custom domain**:
after the next apply, the workers.dev URL for the web app stops serving;
the custom domain is the single origin.

## Docs

- `GETTING_STARTED.md`: custom-domain subsection under Step 8 (vars,
token permission, OAuth-callback reminder), custom-domain entries in the
callback-URL list and redirect-URI troubleshooting, commented vars in
the tfvars sample.
- `terraform.tfvars.example`: notes that workers.dev is disabled and
GitHub/Google OAuth callback URLs must be updated to the new hostname.

## Verification

`terraform fmt -check` and `terraform validate` pass. Validation and
locals exercised end-to-end with `terraform plan` on a minimal harness
under Terraform 1.14.6 (the repo's required floor):

| Scenario | Result |
| --- | --- |
| all unset (defaults) | plans cleanly, `enabled = false` — previously
the coalesce error |
| domain set, `web_platform = "vercel"` | hard error (precondition gate)
|
| domain set, zone id unset | hard error (precondition gate) |
| domain + zone, `web_platform = "cloudflare"` | `enabled = true`, host
= custom domain |
| `https://app.example.com` | hard error (bare-hostname rule) |
| domain `""` | plans cleanly, `enabled = false` |

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added support for serving the web app on a Cloudflare custom domain.
* Expanded setup docs with Cloudflare-specific DNS, certificate, and
OAuth callback guidance.

* **Bug Fixes**
* Improved handling of custom-domain settings by normalizing empty
values and whitespace.
* Tightened validation so custom domains must be valid hostnames and
require the related Cloudflare zone setting when used.
* Updated production routing to disable the default `workers.dev` route
when a custom domain is enabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Take over ColeMurray#734 and resolve conflicts with current `main`, preserving
archived-project filtering.
- Use GitLab `namespace.full_path` for repository owner mapping so
nested groups round-trip correctly.
- Preserve nested group separators in manual MR URLs while still
encoding individual path segments.

## Test plan
- `npm test -w @open-inspect/control-plane -- gitlab-provider.test.ts`
- `npm run typecheck -w @open-inspect/control-plane`

---
*Created with
[Open-Inspect](https://open-inspect-prod.vercel.app/session/8b33d071aa6249421aeed1640d5ca6f7)*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed repository ownership and listing for projects inside nested
GitLab groups.
* Improved manually generated merge request links so nested group paths
open correctly.
* Preserved archived repository filtering while updating project path
handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Mikhail Dubov <mikhail@chattermill.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants