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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .agents/skills/scope-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,44 @@ The `API_PORT` variable is defined in the project `.env` file (default: `3116`).

### Authentication

Most endpoints require a bearer token. Pass it as:
For authenticated calls, pass the existing **IdP access token** unchanged:
```
-H "Authorization: Bearer $SCOPE_TOKEN"
```

If the user has the SCOPE CLI configured, the token may be available via environment or the CLI config.
Use the caller-provided `SCOPE_TOKEN` when available; never print/log the token or
put it in a URL. There is no Scope session JWT, `/auth/login`, or token exchange.
The API verifies signature/claims before consulting its user-access cache.

**Enrollment is explicit.** Already-enrolled bearer callers remain compatible.
For a new identity, intentionally call this once before other authenticated calls:

```bash
curl --fail-with-body -sS \
-X POST \
-H "Authorization: Bearer $SCOPE_TOKEN" \
-H "Cache-Control: no-store" \
"http://localhost:$API_PORT/api/v1/users/me"
```

Only POST `/api/v1/users/me` may JIT-create the user or update profile,
`lastLoginAt`, and eligible bootstrap-admin promotion. This POST has side effects:
**never prefetch or poll it**, and do not silently invoke it as an ordinary lookup
retry. `lastLoginAt` records this explicit upsert, not proof of an interactive login.
For normal identity checks use GET `/api/v1/users/me`; Invalid/repeated/structured
login values return `400`; HEAD never enrolls.

Plain `/me` and other authenticated routes resolve an existing active Scope UUID/role
from Redis; miss/unavailability reads Mongo by exact `(idp, tid, oid)` and warms the
cache, without JIT. Missing users return `403 user_not_enrolled`, disabled users
`403 user_disabled`, reserved/invalid principals `401`; required Mongo/JWKS outages
return `503`, unexpected errors `500`. Do not interpret a cache miss as a denial or
turn a verified-identity denial into anonymous access.

The fixed/non-sliding cache TTL (`AUTH_USER_CACHE_TTL_SECONDS`, default 300 seconds)
means DB-only role/disable changes may not be visible until expiry. Public endpoints
and existing no-token/auth-disabled anonymous rollout remain supported; this is not
full route RBAC. See [the auth contract](../../../docs/architecture/auth-rbac.md).

### Before Making API Calls

Expand All @@ -45,6 +77,7 @@ curl -s http://localhost:$API_PORT/openapi.json | jq '.paths["/api/v1/requests"]
### Key Endpoint Groups

- **System**: `/health`, `/ready`, `/about`, `/api/v1/version`
- **Identity**: GET `/api/v1/users/me` (read-only); POST `/api/v1/users/me` (explicit enrollment/login refresh)
- **Requests & Runs**: `/api/v1/requests/*` (create, cancel, retry, pause, resume, bulk ops, logs, HAR, video, snapshots, tool-calls)
- **Skills**: `/api/v1/skills/*` (discover, search, external, resolve, revisions)
- **Agents**: `/api/v1/agents/*` (CRUD, versions)
Expand Down
34 changes: 34 additions & 0 deletions .agents/skills/scope-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,40 @@ Run it with `pnpm cli` from the repository root (`scope-core`).

---

## Authentication and enrollment

The CLI's shared API transport can attach a caller-provided `SCOPE_TOKEN` containing
an **IdP access token**. Already-enrolled users keep using that bearer unchanged;
there is no Scope-token exchange or new CLI login implementation in this milestone.
Do not assume the deferred `scope auth login`/keychain commands exist.

A new identity must explicitly call **`POST /api/v1/users/me`** using its
IdP bearer before ordinary authenticated commands. Use the configured Scope API URL:

```bash
curl --fail-with-body -sS \
-X POST \
-H "Authorization: Bearer $SCOPE_TOKEN" \
-H "Cache-Control: no-store" \
"${SCOPE_API_URL%/}/api/v1/users/me"
```

This POST creates/refreshes the user, profile, `lastLoginAt`, and eligible bootstrap
promotion; never prefetch, poll, or automatically use it to recover an ordinary
lookup. GET `/users/me` only checks existing access. `403 user_not_enrolled` calls
for explicit enrollment; `403 user_disabled` is a denial, not a refresh-token prompt.
Invalid/expired bearer → `401`; required Mongo/JWKS outage → `503`.

All non-public bearer calls verify the IdP token before Redis/Mongo resolution.
The active-user cache is fixed/non-sliding (300 seconds by default), so DB-only
role/disable edits may remain stale until expiry. No raw bearer is cached by the API.
Never echo tokens or include them in URLs/debug output. Public and anonymous rollout
behavior is unchanged; full RBAC and interactive CLI auth remain deferred.

See [Authentication & RBAC](../../../docs/architecture/auth-rbac.md).

---

## Quick Reference

In order to get the full updated reference, run `pnpm cli --help` or `pnpm cli <command> --help` for specific commands. Below is a summary of the most common commands.
Expand Down
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ ctrf
.env.*
!.env.example

# Local development TLS material is mounted at runtime, never built into images.
.certs

# Documentation (not needed in images)
docs

Expand Down
3 changes: 3 additions & 0 deletions .env.base
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ AZURITE_BLOB_PORT=10100
AZURITE_QUEUE_PORT=10200
AZURITE_TABLE_PORT=10300
API_PORT=3100
# Host-side Node inspector port for the API in docker-compose.dev.yml.
# The inspector is available only on localhost; the container uses port 9229.
API_DEBUG_PORT=9200
# entra-local (dev auth emulator) host port. Offset per worktree so each
# composition gets a unique host port; the advertised origin (PUBLIC_ORIGIN)
# and MSAL authority follow this port. Container still binds 8443 internally.
Expand Down
48 changes: 48 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,51 @@ FEEDBACK_MAX_CRITERIA=1
# Whether to guard against hinting about descendant criteria (default: true)
# Prevents feedback from mentioning requirements not yet introduced
FEEDBACK_DESCENDANT_GUARD=true

# ---------------------------------------------------------------------------
# API Authentication (Microsoft Entra ID) — identity-only, non-breaking
# ---------------------------------------------------------------------------
# Leave AUTH_PROVIDER unset to keep auth disabled: the API boots and treats
# every caller as anonymous when all IdP settings are absent. Configured clients
# enroll through GET /api/v1/users/me?login=true, then use IdP bearer tokens on
# ordinary requests. Active Scope users/roles are cached in Redis.
# See ENV_VARIABLES.md → "API Authentication" for details.

# Identity provider. Set to `entra` to enable token verification.
# AUTH_PROVIDER=entra

# OIDC authority for JWKS discovery + issuer validation (multi-tenant example).
# Point at the entra-local emulator for offline development.
# AUTH_AUTHORITY=https://login.microsoftonline.com/common

# Per-tenant issuer template ({tenantid} is substituted from the token `tid`).
# Override only for a self-hosted issuer, e.g. the entra-local emulator:
# https://localhost:8443/{tenantid}/v2.0
# AUTH_ISSUER_TEMPLATE=https://login.microsoftonline.com/{tenantid}/v2.0

# Explicit JWKS URI. Leave unset to derive it as <AUTH_AUTHORITY>/discovery/v2.0/keys.
# Every signing key must publish an `issuer` matching the token issuer (with
# `{tenantid}` substitution supported); missing issuer metadata is rejected.
# AUTH_JWKS_URI=

# The API App Registration (client) ID — verified as the token audience (aud).
# AUTH_API_CLIENT_ID=00000000-0000-0000-0000-000000000000

# Public client ID advertised to the CLI for interactive sign-in.
# AUTH_CLI_CLIENT_ID=00000000-0000-0000-0000-000000000000

# Public client ID advertised to the Portal for interactive sign-in.
# AUTH_PORTAL_CLIENT_ID=00000000-0000-0000-0000-000000000000

# Scopes the CLI/Portal request for the API access token.
# AUTH_SCOPES=api://00000000-0000-0000-0000-000000000000/access

# Promote-only admin bootstrap: comma-separated `${idp}:${tenant}/${subject}`.
# AUTH_BOOTSTRAP_ADMINS=

# Required tenant allowlist when AUTH_BOOTSTRAP_ADMINS is configured.
# AUTH_BOOTSTRAP_TENANTS=

# Fixed active-user cache lifetime, in seconds (positive integer, default 300).
# Cache hits do not extend expiry; Redis outages fall back to MongoDB.
# AUTH_USER_CACHE_TTL_SECONDS=300
71 changes: 71 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,74 @@ AZURE_AI_INFERENCE_API_KEY=
# Defaults to gpt-4.1 inside the API when unset.
# ---------------------------------------------------------------------------
LLM_MODEL=

# ---------------------------------------------------------------------------
# API Authentication — cmaneu/entra-local emulator defaults
#
# Ready-to-use values for local sign-in against the `entra-local` emulator
# (https://github.com/cmaneu/entra-local). Uncomment the block below to have
# the API verify Entra ID access tokens minted by the emulator and attach
# `req.user` to each request. Leave it commented to keep auth disabled (the API
# still boots and treats every caller as anonymous). See ENV_VARIABLES.md →
# "API Authentication" for the full variable reference.
#
# Start the emulator (HTTPS on :8443, seeded tenant + apps + users):
# docker pull ghcr.io/cmaneu/entra-local
# docker run -p 8443:8443 -v entra-local-data:/app/data ghcr.io/cmaneu/entra-local
#
# The emulator serves a self-signed certificate. `jose` fetches the JWKS over
# HTTPS, so for local dev either trust the emulator CA or, as a quick shortcut,
# set NODE_TLS_REJECT_UNAUTHORIZED=0 for the api service (dev-only — never in
# production). The emulator advertises issuer + JWKS under
# https://localhost:8443/<tenant>/v2.0.
#
# Seeded identities (see the entra-local docs):
# tenant 11111111-1111-1111-1111-111111111111
# API app cccccccc-0000-0000-0000-000000000005 (exposes access_as_user) -> token `aud`
# SPA app cccccccc-0000-0000-0000-000000000001 (public client, redirect https://localhost:3000)
# user alice@entralocal.dev -> oid aaaaaaaa-0000-0000-0000-000000000001 (bootstrap candidate below)
# user bob@entralocal.dev -> oid aaaaaaaa-0000-0000-0000-000000000002
# ---------------------------------------------------------------------------
# AUTH_PROVIDER=entra

# OIDC authority — the emulator issuer base for the seeded tenant. In a worktree
# with the normal port offset, this is usually `https://localhost:8501/...` not
# the base `:8443` emulator port exposed in the upstream docs.
# AUTH_AUTHORITY=https://localhost:8501/11111111-1111-1111-1111-111111111111

# Issuer template — REQUIRED for entra-local, since its issuer differs from the
# Entra-cloud default (`https://login.microsoftonline.com/{tenantid}/v2.0`).
# `{tenantid}` is substituted from each token's `tid` claim.
# AUTH_ISSUER_TEMPLATE=https://localhost:8501/{tenantid}/v2.0

# The API App Registration (client) ID — verified as the token audience (aud).
# For entra-local this is the seeded resource API app that exposes access_as_user.
# AUTH_API_CLIENT_ID=cccccccc-0000-0000-0000-000000000005

# Public client ID advertised to the CLI for interactive sign-in.
# AUTH_CLI_CLIENT_ID=cccccccc-0000-0000-0000-000000000001

# Public client ID advertised to the Portal for interactive sign-in.
# AUTH_PORTAL_CLIENT_ID=cccccccc-0000-0000-0000-000000000001

# Scope the CLI/Portal request when acquiring an API access token.
# AUTH_SCOPES=api://cccccccc-0000-0000-0000-000000000005/access_as_user
Comment thread
cmaneu marked this conversation as resolved.

# Optional explicit JWKS URI. Docker Compose defaults this to the emulator's
# internal address (`https://entra-local:8443/.../keys`). For a native API
# process, use the worktree's host-facing ENTRA_LOCAL_PORT instead. Scope
# requires every signing key to publish its per-tenant `issuer`; use an updated
# entra-local image that includes this JWKS extension.
# AUTH_JWKS_URI=https://localhost:8501/11111111-1111-1111-1111-111111111111/discovery/v2.0/keys

# Promote-only admin bootstrap: comma-separated `${idp}:${tenant}/${subject}`.
# GET /api/v1/users/me?login=true promotes alice only if her API access token
# asserts email_verified=true; otherwise the default role remains `user`.
# AUTH_BOOTSTRAP_ADMINS=entra:11111111-1111-1111-1111-111111111111/aaaaaaaa-0000-0000-0000-000000000001

# Required tenant allowlist when AUTH_BOOTSTRAP_ADMINS is configured.
# AUTH_BOOTSTRAP_TENANTS=11111111-1111-1111-1111-111111111111

# Fixed active-user Redis cache lifetime; normal requests never update lastLoginAt.
# Redis keys are namespaced by MONGO_DATABASE and verified provider/tid/oid.
# AUTH_USER_CACHE_TTL_SECONDS=300
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,16 @@ jobs:
target: ${{ matrix.image.target || '' }}
push: true
tags: ${{ steps.tags.outputs.tags }}
build-args: ${{ steps.versions.outputs.build_args }}
build-args: |
${{ steps.versions.outputs.build_args }}
VITE_AUTH_CLIENT_ID=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_CLIENT_ID || '' }}
VITE_AUTH_AUTHORITY=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_AUTHORITY || '' }}
VITE_AUTH_KNOWN_AUTHORITIES=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_KNOWN_AUTHORITIES || '' }}
VITE_AUTH_SCOPES=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_SCOPES || '' }}
VITE_AUTH_PROTOCOL_MODE=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_PROTOCOL_MODE || '' }}
VITE_AUTH_REDIRECT_URI=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_REDIRECT_URI || '' }}
VITE_AUTH_POST_LOGOUT_REDIRECT_URI=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_POST_LOGOUT_REDIRECT_URI || '' }}
VITE_AUTH_CACHE_LOCATION=${{ matrix.image.name == 'portal' && vars.VITE_AUTH_CACHE_LOCATION || '' }}
cache-from: type=gha,scope=${{ matrix.image.name }},ignore-error=true
cache-to: type=gha,mode=max,scope=${{ matrix.image.name }}

Expand Down
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
"name": "Attach API (Docker)",
"type": "node",
"request": "attach",
"address": "localhost",
"port": "${input:apiDebugPort}",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"sourceMaps": true,
"restart": true,
"timeout": 30000,
"skipFiles": [
"<node_internals>/**",
"${workspaceFolder}/**/node_modules/**"
]
}
],
"inputs": [
{
"id": "apiDebugPort",
"type": "promptString",
"description": "API_DEBUG_PORT from this worktree's .env file",
"default": "9200"
}
]
}
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ Express.js REST server. Orchestrates runs, streams logs via SSE, manages criteri
- SSE + Change Streams pattern: [docs/research/realtime-data-flow.md](docs/research/realtime-data-flow.md)
- Environment variables: [ENV_VARIABLES.md](ENV_VARIABLES.md)

> **Authentication invariant:** verify the unchanged IdP bearer before any user-access
> cache lookup. Only `POST /api/v1/users/me` calls
> `UserAccessResolver.enrollOnLogin()` for JIT/profile/lastLogin/bootstrap writes.
> Every `GET /users/me` and other routes use `resolveExisting()` (Redis hit: no Mongo;
> miss/outage: exact identity read, never upsert). Missing/disabled identities deny
> access, never become anonymous. Preserve existing no-token/public rollout; full
> RBAC and Scope internal tokens remain deferred. See
> [auth-rbac.md](docs/architecture/auth-rbac.md) before changing this boundary.

### Workers (`apps/workers/`)

Each worker implements the same queue-processor interface but adapts a different coding agent:
Expand All @@ -76,6 +85,12 @@ Evaluation engine that scores agent output against a criteria DAG (directed acyc

React 19 web UI with Vite, Tailwind CSS, Radix UI (shadcn/ui), TanStack Query, and XYFlow for criteria DAG visualization. Communicates with the API via REST and SSE.

`AuthProvider` owns the Scope-user handshake: callback → `POST /users/me`;
cached-account reload → plain `/users/me`. Gate all eager queries (including
providers outside `RequireAuth`) until ready; do not treat MSAL account claims as
the Scope UUID/role. Deduplicate account/login work and cancel it on account change
or logout. The enrollment POST is no-store and must never be prefetched/polled.

- Real-time data flow: [docs/research/realtime-data-flow.md](docs/research/realtime-data-flow.md)

> **Storybook**: When adding or modifying portal components, update the corresponding Storybook stories. Use the `storybook` skill for guidance.
Expand Down Expand Up @@ -217,6 +232,7 @@ not open the PR against the fork unless the user explicitly asks you to.
| [docs/architecture/overview.md](docs/architecture/overview.md) | System architecture, component interactions, data flow |
| [docs/architecture/app-design.md](docs/architecture/app-design.md) | Data models, API design, package dependency graph |
| [docs/architecture/data-organization-projects.md](docs/architecture/data-organization-projects.md) | Projects (a single container) to isolate/group data within a cluster; composes with data-tags and auth-rbac |
| [docs/architecture/auth-rbac.md](docs/architecture/auth-rbac.md) | Explicit-login IdP auth, Redis user-access cache, Portal handshake; deferred RBAC/internal-token roadmap |
| [docs/architecture/vscode-web-worker.md](docs/architecture/vscode-web-worker.md) | XState chat machine, GitHub auth flow, ARIA snapshots |
| [docs/architecture/token-manager.md](docs/architecture/token-manager.md) | Token storage, validation, round-robin distribution |
| [docs/architecture/criteria-provider.md](docs/architecture/criteria-provider.md) | CriteriaProvider abstraction, filesystem vs REST backends |
Expand Down
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ pnpm dev:coder-acp-copilot # A single worker natively (pnpm dev:<service-
pnpm open:portal # Open the portal in your browser
```

### Debug the Docker development API

`pnpm docker:dev:portal` starts the API with the Node inspector enabled. To
debug API TypeScript while retaining the Docker stack and hot reload:

1. Run `pnpm docker:dev:portal` and wait for the API to start.
2. Read `API_DEBUG_PORT` from the generated `.env` file (it is worktree-specific).
3. In VS Code, select **Attach API (Docker)** from **Run and Debug**, enter that
port, and start debugging.
4. Set breakpoints in the workspace source under `apps/api/src`, not in a copied
or attached snapshot of the file.

The debugger maps the container's `/app` tree to the workspace and reconnects
when `tsx watch` restarts the API after a source change. The inspector is
published on `127.0.0.1` only.

The CLI is the primary interface for CI/CD and power users:

```bash
Expand Down
Loading
Loading