From 21920db60bab820762400328aa0c656d8b7d38a6 Mon Sep 17 00:00:00 2001 From: Mathew Dunne Date: Wed, 30 Sep 2026 22:54:38 -0400 Subject: [PATCH 1/4] feat(auth): Auth0 SSO sign-in (#24) Add Auth0 as a third login provider through Better Auth's genericOAuth plugin, enabled by AUTH0_DOMAIN/CLIENT_ID/CLIENT_SECRET. Auth0 users skip the allowlist; a roles claim in the ID token (AUTH0_ROLES_CLAIM) maps AUTH0_ADMIN_ROLE_NAME to admin and AUTH0_USER_ROLE_NAME to student and is re-read at every sign-in. No role means denied: new users in user.create.before, returning users in session.create.before so no session or cookie is issued. CODERUNNER_ADMIN_EMAIL still grants admin. Decision 044; setup guide in docs/deploying/oauth-credentials.md. Co-Authored-By: Claude Opus 5.5 --- .env.example | 9 + AGENTS.md | 15 +- apps/control/src/__tests__/auth.test.ts | 39 +++ apps/control/src/__tests__/auth0.test.ts | 291 +++++++++++++++++++++++ apps/control/src/auth/auth.ts | 139 ++++++++++- apps/control/src/auth/providers.ts | 63 ++++- apps/control/src/config.ts | 21 ++ apps/control/src/storage.ts | 10 +- apps/web/src/lib/auth-client.ts | 2 + apps/web/src/routes/LoginPage.tsx | 64 +++-- docs/about/security-model.md | 8 + docs/decisions/044-auth0-sso.md | 55 +++++ docs/deploying/oauth-credentials.md | 72 +++++- docs/reference/configuration.md | 8 +- packages/contracts/src/index.test.ts | 5 + packages/contracts/src/index.ts | 2 +- 16 files changed, 758 insertions(+), 45 deletions(-) create mode 100644 apps/control/src/__tests__/auth0.test.ts create mode 100644 docs/decisions/044-auth0-sso.md diff --git a/.env.example b/.env.example index 4c797633..ffed73d9 100644 --- a/.env.example +++ b/.env.example @@ -50,11 +50,20 @@ # GITHUB_CLIENT_SECRET= # GitHub OAuth app client secret # GOOGLE_CLIENT_ID= # Google OAuth client ID # GOOGLE_CLIENT_SECRET= # Google OAuth client secret +# AUTH0_DOMAIN= # Auth0 tenant domain, e.g. myteam.us.auth0.com +# AUTH0_CLIENT_ID= # Auth0 application client ID +# AUTH0_CLIENT_SECRET= # Auth0 application client secret +# AUTH0_ROLES_CLAIM=https://coderunner/roles # ID-token claim holding Auth0 role names +# AUTH0_USER_ROLE_NAME=user # Auth0 role that signs in as a student +# AUTH0_ADMIN_ROLE_NAME=admin # Auth0 role that signs in as an admin # # At least one OAuth provider must be configured for login to work. # Register apps at your provider and set the callback URL: # GitHub: ${BETTER_AUTH_URL}/api/auth/callback/github # Google: ${BETTER_AUTH_URL}/api/auth/callback/google +# Auth0: ${BETTER_AUTH_URL}/api/auth/oauth2/callback/auth0 +# Auth0 users skip the allowlist; their role comes from the roles claim +# (see docs/deploying/oauth-credentials.md). # # CODERUNNER_ADMIN_EMAIL= # Comma-separated emails that bootstrap admin # # access. Each is added to the allowlist at diff --git a/AGENTS.md b/AGENTS.md index 86846e02..0b00c375 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,19 @@ from `showDriverStation` to make that possible. See `docs/decisions/041-project-preview.md` and [`docs/using-coderunner.md`](./docs/using-coderunner.md). +**Auth0 SSO (post-V2):** a third login provider beside GitHub and Google, +enabled by `AUTH0_DOMAIN`/`AUTH0_CLIENT_ID`/`AUTH0_CLIENT_SECRET` and wired +through Better Auth's `genericOAuth` plugin (callback +`/api/auth/oauth2/callback/auth0`). Auth0 users skip the allowlist; a roles +claim in the ID token (`AUTH0_ROLES_CLAIM`, set by a Post-Login Action) maps +`AUTH0_ADMIN_ROLE_NAME` → admin and `AUTH0_USER_ROLE_NAME` → student, and is +re-read at every sign-in. No role means denied, enforced in +`user.create.before` (new) and `session.create.before` (returning) — not the +after hook, whose throws Better Auth loses behind the callback redirect. +`CODERUNNER_ADMIN_EMAIL` still grants admin. See +`docs/decisions/044-auth0-sso.md` and +[`docs/deploying/oauth-credentials.md`](./docs/deploying/oauth-credentials.md). + **Containerized control plane (post-V2):** the control plane ships as a Docker image (`containers/control/Dockerfile` → `ghcr.io/mathewdunne/coderunner-control`) and is deployed with docker compose (`docker-compose.yml` base + @@ -134,7 +147,7 @@ arch-independent). See `docs/decisions/035-multi-arch-images-and-workflow-split. ## Key References - `docs/` + `website/` — docs site content and Docusaurus config; published at `https://mathewdunne.github.io/CodeRunner/`; run `bun run docs:dev` to browse locally, `bun run docs:build` to build. -- `docs/decisions/` — all architecture decision logs (011–041 active; 001–010 archived under `docs/decisions/archive/`). +- `docs/decisions/` — all architecture decision logs (011–044 active; 001–010 archived under `docs/decisions/archive/`). - Pinned AdvantageScope submodule: `vendor/AdvantageScope` at tag `v26.0.2`. ## Commands diff --git a/apps/control/src/__tests__/auth.test.ts b/apps/control/src/__tests__/auth.test.ts index 49950823..2bbf1ee3 100644 --- a/apps/control/src/__tests__/auth.test.ts +++ b/apps/control/src/__tests__/auth.test.ts @@ -381,10 +381,46 @@ describe("auth provider discovery", () => { githubClientSecret: "github-client-secret", googleClientId: "", googleClientSecret: "", + auth0Domain: "", + auth0ClientId: "", + auth0ClientSecret: "", }, ); }); + test("lists auth0 only when its domain, client ID, and secret are all set", async () => { + const others = { + githubClientId: "", + githubClientSecret: "", + googleClientId: "", + googleClientSecret: "", + }; + const providersFor = (options: Record) => + withApp(async (app) => { + const response = await app.fetch( + new Request("http://localhost/api/auth/providers"), + ); + return ((await response.json()) as { providers: string[] }).providers; + }, options); + + expect( + await providersFor({ + ...others, + auth0Domain: "tenant.auth0.test", + auth0ClientId: "id", + auth0ClientSecret: "secret", + }), + ).toEqual(["auth0"]); + expect( + await providersFor({ + ...others, + auth0Domain: "tenant.auth0.test", + auth0ClientId: "id", + auth0ClientSecret: "", + }), + ).toEqual([]); + }); + test("returns an empty list when no OAuth providers are configured", async () => { await withApp( async (app) => { @@ -399,6 +435,9 @@ describe("auth provider discovery", () => { githubClientSecret: "", googleClientId: "", googleClientSecret: "", + auth0Domain: "", + auth0ClientId: "", + auth0ClientSecret: "", }, ); }); diff --git a/apps/control/src/__tests__/auth0.test.ts b/apps/control/src/__tests__/auth0.test.ts new file mode 100644 index 00000000..097cea6a --- /dev/null +++ b/apps/control/src/__tests__/auth0.test.ts @@ -0,0 +1,291 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import type { ControlApp } from "../app"; +import { resolveAuth0Role } from "../auth/providers"; +import { loadControlConfig } from "../config"; +import { withApp } from "./helpers"; + +const AUTH0_DOMAIN = "tenant.auth0.test"; +const AUTH0_ORIGIN = `https://${AUTH0_DOMAIN}`; +const BASE_URL = "http://localhost:4000"; +const ROLES_CLAIM = "https://coderunner/roles"; + +const auth0Options = { + auth0Domain: AUTH0_DOMAIN, + auth0ClientId: "auth0-client-id", + auth0ClientSecret: "auth0-client-secret", +}; + +function unsignedJwt(claims: Record): string { + const encode = (value: unknown) => + Buffer.from(JSON.stringify(value)).toString("base64url"); + return `${encode({ alg: "none", typ: "JWT" })}.${encode(claims)}.`; +} + +// Claims the fake Auth0 token endpoint puts in the next ID token it issues. +let nextIdTokenClaims: Record = {}; +const realFetch = globalThis.fetch; + +beforeAll(() => { + // Stand in for the Auth0 tenant: discovery + token endpoint. Better Auth + // talks to these over fetch; everything else passes through. + globalThis.fetch = (async ( + input: Parameters[0], + init?: Parameters[1], + ) => { + const url = new URL( + typeof input === "string" || input instanceof URL ? input : input.url, + ); + if (url.origin !== AUTH0_ORIGIN) { + return realFetch(input, init); + } + if (url.pathname === "/.well-known/openid-configuration") { + return Response.json({ + issuer: `${AUTH0_ORIGIN}/`, + authorization_endpoint: `${AUTH0_ORIGIN}/authorize`, + token_endpoint: `${AUTH0_ORIGIN}/oauth/token`, + userinfo_endpoint: `${AUTH0_ORIGIN}/userinfo`, + }); + } + if (url.pathname === "/oauth/token") { + return Response.json({ + access_token: "auth0-access-token", + token_type: "Bearer", + expires_in: 3600, + id_token: unsignedJwt({ + iss: `${AUTH0_ORIGIN}/`, + aud: auth0Options.auth0ClientId, + ...nextIdTokenClaims, + }), + }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; +}); + +afterAll(() => { + globalThis.fetch = realFetch; +}); + +/** Run the full Auth0 sign-in round trip; returns the callback response. */ +async function auth0Login( + app: ControlApp, + claims: Record, +): Promise { + nextIdTokenClaims = claims; + const signIn = await app.fetch( + new Request(`${BASE_URL}/api/auth/sign-in/oauth2`, { + method: "POST", + headers: { "content-type": "application/json", origin: BASE_URL }, + body: JSON.stringify({ + providerId: "auth0", + callbackURL: "/", + errorCallbackURL: "/login", + }), + }), + ); + expect(signIn.status).toBe(200); + const { url } = (await signIn.json()) as { url: string }; + const state = new URL(url).searchParams.get("state"); + const cookies = signIn.headers + .getSetCookie() + .map((cookie) => cookie.split(";")[0]) + .join("; "); + return app.fetch( + new Request( + `${BASE_URL}/api/auth/oauth2/callback/auth0?code=auth-code&state=${state}`, + { headers: { cookie: cookies } }, + ), + ); +} + +function auth0User(sub: string, email: string, roles?: unknown) { + return { + sub, + email, + email_verified: true, + name: email.split("@")[0], + ...(roles === undefined ? {} : { [ROLES_CLAIM]: roles }), + }; +} + +function userRow(app: ControlApp, email: string) { + return app.storage.db + .query("SELECT id, role, slug FROM user WHERE email = ?") + .get(email) as { id: string; role: string; slug: string } | null; +} + +function sessionCount(app: ControlApp, userId: string): number { + return ( + app.storage.db + .query("SELECT COUNT(*) AS count FROM session WHERE userId = ?") + .get(userId) as { count: number } + ).count; +} + +/** The role in the session that `response`'s cookies grant, or null. */ +async function sessionRole( + app: ControlApp, + response: Response, +): Promise { + const cookie = response.headers + .getSetCookie() + .map((value) => value.split(";")[0]) + .join("; "); + const session = await app.fetch( + new Request(`${BASE_URL}/api/auth/get-session`, { headers: { cookie } }), + ); + const body = (await session.json()) as { user?: { role?: string } } | null; + return body?.user?.role ?? null; +} + +function expectSignedIn(response: Response) { + expect(response.status).toBe(302); + expect(response.headers.get("location")).not.toContain("error"); +} + +describe("resolveAuth0Role", () => { + const config = loadControlConfig({ adminEmails: ["coach@test.local"] }); + + test("maps the configured role names", () => { + expect( + resolveAuth0Role({ [ROLES_CLAIM]: ["user"] }, "a@x.test", config), + ).toBe("student"); + expect( + resolveAuth0Role({ [ROLES_CLAIM]: ["admin"] }, "a@x.test", config), + ).toBe("admin"); + expect( + resolveAuth0Role( + { [ROLES_CLAIM]: ["user", "admin"] }, + "a@x.test", + config, + ), + ).toBe("admin"); + expect( + resolveAuth0Role({ [ROLES_CLAIM]: "user" }, "a@x.test", config), + ).toBe("student"); + }); + + test("returns null without a matching role", () => { + expect(resolveAuth0Role({}, "a@x.test", config)).toBeNull(); + expect( + resolveAuth0Role({ [ROLES_CLAIM]: [] }, "a@x.test", config), + ).toBeNull(); + expect( + resolveAuth0Role({ [ROLES_CLAIM]: ["guest"] }, "a@x.test", config), + ).toBeNull(); + expect( + resolveAuth0Role({ roles: ["user"] }, "a@x.test", config), + ).toBeNull(); + }); + + test("admin emails are admin regardless of roles", () => { + expect(resolveAuth0Role({}, "Coach@Test.local", config)).toBe("admin"); + }); + + test("honours custom claim and role names", () => { + const custom = loadControlConfig({ + auth0RolesClaim: "roles", + auth0UserRoleName: "member", + auth0AdminRoleName: "teacher", + }); + expect(resolveAuth0Role({ roles: ["member"] }, "a@x.test", custom)).toBe( + "student", + ); + expect(resolveAuth0Role({ roles: ["teacher"] }, "a@x.test", custom)).toBe( + "admin", + ); + expect( + resolveAuth0Role({ roles: ["user"] }, "a@x.test", custom), + ).toBeNull(); + }); +}); + +describe("Auth0 sign-in", () => { + test("a new user with the user role gets in without being on the allowlist", async () => { + await withApp(async (app) => { + const response = await auth0Login( + app, + auth0User("auth0|student", "student@team.test", ["user"]), + ); + expectSignedIn(response); + const user = userRow(app, "student@team.test"); + expect(user?.role).toBe("student"); + expect(user?.slug).toBe("student"); + const workspace = app.storage.db + .query("SELECT slug FROM workspaces WHERE user_id = ?") + .get(user?.id ?? ""); + expect(workspace).toBeTruthy(); + }, auth0Options); + }); + + test("a new user with the admin role becomes admin", async () => { + await withApp(async (app) => { + expectSignedIn( + await auth0Login( + app, + auth0User("auth0|coach", "coach@team.test", ["admin"]), + ), + ); + expect(userRow(app, "coach@team.test")?.role).toBe("admin"); + }, auth0Options); + }); + + test("a new user with no role is denied and no user row is created", async () => { + await withApp(async (app) => { + const response = await auth0Login( + app, + auth0User("auth0|nobody", "nobody@team.test"), + ); + expect(response.status).toBe(302); + expect(response.headers.get("location")).toContain("/login?error="); + expect(userRow(app, "nobody@team.test")).toBeNull(); + }, auth0Options); + }); + + test("CODERUNNER_ADMIN_EMAIL is admin even without Auth0 roles", async () => { + await withApp( + async (app) => { + expectSignedIn( + await auth0Login(app, auth0User("auth0|boss", "boss@team.test")), + ); + expect(userRow(app, "boss@team.test")?.role).toBe("admin"); + }, + { ...auth0Options, adminEmails: ["boss@team.test"] }, + ); + }); + + test("roles are re-read on every sign-in", async () => { + await withApp(async (app) => { + const email = "mover@team.test"; + expectSignedIn( + await auth0Login(app, auth0User("auth0|mover", email, ["user"])), + ); + expect(userRow(app, email)?.role).toBe("student"); + + // Promotion lands in the DB and in the session the sign-in hands out. + const promoted = await auth0Login( + app, + auth0User("auth0|mover", email, ["admin"]), + ); + expectSignedIn(promoted); + expect(userRow(app, email)?.role).toBe("admin"); + expect(await sessionRole(app, promoted)).toBe("admin"); + + expectSignedIn( + await auth0Login(app, auth0User("auth0|mover", email, ["user"])), + ); + expect(userRow(app, email)?.role).toBe("student"); + + // Role removed in Auth0: denied before any session or cookie exists. + const userId = userRow(app, email)?.id ?? ""; + const sessionsBefore = sessionCount(app, userId); + const denied = await auth0Login(app, auth0User("auth0|mover", email)); + expect(denied.status).toBe(302); + expect(denied.headers.get("location")).toBe( + "/login?error=Your_Auth0_account_has_no_CodeRunner_role._Ask_your_coach_for_access.", + ); + expect(sessionCount(app, userId)).toBe(sessionsBefore); + expect(await sessionRole(app, denied)).toBeNull(); + }, auth0Options); + }); +}); diff --git a/apps/control/src/auth/auth.ts b/apps/control/src/auth/auth.ts index 70779d52..d4e92f77 100644 --- a/apps/control/src/auth/auth.ts +++ b/apps/control/src/auth/auth.ts @@ -3,9 +3,9 @@ * * Creates and configures the betterAuth instance with: * - SQLite database (shared with AppStorage) - * - GitHub + Google OAuth providers + * - GitHub + Google OAuth providers, plus Auth0 via genericOAuth * - Custom user fields: role, slug - * - Email allowlist enforcement via hooks + * - Email allowlist enforcement via hooks (Auth0 users: role claim instead) * - 14-day session expiry with daily refresh */ @@ -15,10 +15,76 @@ import { APIError, createAuthMiddleware } from "better-auth/api"; import type { ControlConfig } from "../config"; import { getLogger } from "../logging"; import { isEmailAllowed, reloadAllowlist } from "./allowlist"; -import { buildSocialProviders } from "./providers"; +import { + buildAuth0Plugin, + buildSocialProviders, + resolveAuth0Role, +} from "./providers"; const log = getLogger("auth"); +const ROSTER_MESSAGE = + "Your email is not on the roster. Ask your coach to add you."; +const AUTH0_NO_ROLE_MESSAGE = + "Your Auth0 account has no CodeRunner role. Ask your coach for access."; + +/** True for the genericOAuth callback of the Auth0 provider. */ +function isAuth0Callback( + ctx: + | { + path?: string | undefined; + params?: Record | undefined; + } + | null + | undefined, +): boolean { + return ( + ctx?.path === "/oauth2/callback/:providerId" && + ctx.params?.providerId === "auth0" + ); +} + +/** + * Read an ID token's payload. No signature check: Better Auth received the + * token directly from Auth0's token endpoint and trusts it the same way. + */ +function decodeIdTokenClaims(idToken: string): Record { + try { + const payload = idToken.split(".")[1] ?? ""; + const claims = JSON.parse(Buffer.from(payload, "base64url").toString()); + return claims && typeof claims === "object" ? claims : {}; + } catch { + return {}; + } +} + +type Auth0Adapter = { + findUserById(userId: string): Promise<{ email: string } | null>; + findAccounts( + userId: string, + ): Promise<{ providerId: string; idToken?: string | null | undefined }[]>; +}; + +/** + * The role carried by the ID token Better Auth stored for the user's Auth0 + * account on this sign-in (tokens are refreshed before the session is made). + */ +async function currentAuth0Role( + adapter: Auth0Adapter, + userId: string, + config: ControlConfig, +): Promise<"admin" | "student" | null> { + const [user, accounts] = await Promise.all([ + adapter.findUserById(userId), + adapter.findAccounts(userId), + ]); + const idToken = accounts.find((a) => a.providerId === "auth0")?.idToken; + if (!user || !idToken) { + return null; + } + return resolveAuth0Role(decodeIdTokenClaims(idToken), user.email, config); +} + /** * Reload allowlist.json from disk before enforcing it, so CLI edits * (`coderunner allowlist add`) take effect on the next sign-in attempt @@ -61,6 +127,7 @@ export function createAuth( callbacks: AuthCallbacks, ) { const socialProviders = config.demo ? {} : buildSocialProviders(config); + const auth0Plugin = config.demo ? null : buildAuth0Plugin(config); const options: BetterAuthOptions = { database: db, @@ -68,6 +135,7 @@ export function createAuth( basePath: "/api/auth", secret: config.sessionSecret, socialProviders, + plugins: auth0Plugin ? [auth0Plugin] : [], session: { expiresIn: 14 * 24 * 60 * 60, // 14 days in seconds updateAge: 24 * 60 * 60, // refresh session expiry daily @@ -102,7 +170,27 @@ export function createAuth( databaseHooks: { user: { create: { - before: async (user) => { + before: async (user, ctx) => { + const slug = slugFromEmail(user.email); + if (isAuth0Callback(ctx)) { + // Auth0 vets the user; the role claim replaces the allowlist. + // genericOAuth spreads the ID-token claims onto `user`. + const role = resolveAuth0Role(user, user.email, config); + if (!role) { + log.warn("new auth0 user rejected: no role", { + email: user.email, + }); + throw new APIError("FORBIDDEN", { + message: AUTH0_NO_ROLE_MESSAGE, + }); + } + log.info("creating new auth0 user", { + email: user.email, + slug, + role, + }); + return { data: { ...user, slug, role } }; + } // Enforce allowlist on new user creation await refreshAllowlistBeforeCheck(); if (!isEmailAllowed(user.email)) { @@ -110,11 +198,9 @@ export function createAuth( email: user.email, }); throw new APIError("FORBIDDEN", { - message: - "Your email is not on the roster. Ask your coach to add you.", + message: ROSTER_MESSAGE, }); } - const slug = slugFromEmail(user.email); const role = config.adminEmails.includes(user.email.toLowerCase()) ? "admin" : "student"; @@ -129,16 +215,44 @@ export function createAuth( }, }, }, + session: { + create: { + before: async (session, ctx) => { + if (!ctx || !isAuth0Callback(ctx)) { + return; + } + // Every Auth0 sign-in re-checks the role claim. Deny here, before + // any session or cookie exists: Better Auth doesn't catch errors + // from session creation, and an after-hook throw would be lost + // behind the callback's redirect. + const role = await currentAuth0Role( + ctx.context.internalAdapter, + session.userId, + config, + ); + if (!role) { + log.warn("auth0 sign-in rejected: no role", { + userId: session.userId, + }); + const error = AUTH0_NO_ROLE_MESSAGE.replaceAll(" ", "_"); + throw ctx.redirect(`/login?error=${encodeURIComponent(error)}`); + } + }, + }, + }, }, hooks: { after: createAuthMiddleware(async (ctx) => { - // Enforce allowlist on returning users (OAuth callback) - if (ctx.path === "/callback/:id") { + const auth0 = isAuth0Callback(ctx); + + if (ctx.path === "/callback/:id" || auth0) { const newSession = ctx.context.newSession; - if (newSession) { + // Enforce allowlist on returning users (social OAuth callback). + // Auth0 users were checked against their role claim instead. + if (newSession && !auth0) { await refreshAllowlistBeforeCheck(); } - if (newSession && !isEmailAllowed(newSession.user.email)) { + if (newSession && !auth0 && !isEmailAllowed(newSession.user.email)) { log.warn("oauth callback rejected: not on allowlist", { email: newSession.user.email, }); @@ -147,8 +261,7 @@ export function createAuth( newSession.session.token, ); throw new APIError("FORBIDDEN", { - message: - "Your email is not on the roster. Ask your coach to add you.", + message: ROSTER_MESSAGE, }); } diff --git a/apps/control/src/auth/providers.ts b/apps/control/src/auth/providers.ts index f43e746f..8541d7e5 100644 --- a/apps/control/src/auth/providers.ts +++ b/apps/control/src/auth/providers.ts @@ -1,9 +1,11 @@ /** * OAuth provider configuration for Better Auth. * - * Reads GitHub and Google credentials from ControlConfig and builds - * the socialProviders object that betterAuth() expects. + * Reads GitHub, Google, and Auth0 credentials from ControlConfig. GitHub and + * Google are Better Auth social providers; Auth0 goes through the genericOAuth + * plugin (decision 044). */ +import { auth0, genericOAuth } from "better-auth/plugins/generic-oauth"; import type { ControlConfig } from "../config"; export type SocialProviders = { @@ -19,7 +21,7 @@ export type SocialProviders = { }; }; -export type OAuthProvider = "github" | "google"; +export type OAuthProvider = "github" | "google" | "auth0"; export function getEnabledAuthProviders( config: ControlConfig, @@ -34,6 +36,10 @@ export function getEnabledAuthProviders( providers.push("google"); } + if (config.auth0Domain && config.auth0ClientId && config.auth0ClientSecret) { + providers.push("auth0"); + } + return providers; } @@ -60,3 +66,54 @@ export function buildSocialProviders(config: ControlConfig): SocialProviders { return providers; } + +/** The genericOAuth plugin carrying Auth0, or null when Auth0 isn't configured. */ +export function buildAuth0Plugin(config: ControlConfig) { + if (!getEnabledAuthProviders(config).includes("auth0")) { + return null; + } + return genericOAuth({ + config: [ + { + ...auth0({ + domain: config.auth0Domain!, + clientId: config.auth0ClientId!, + clientSecret: config.auth0ClientSecret!, + }), + overrideUserInfo: true, + // Write the role on every sign-in so the user row (and the session + // cookie cache built from it) carries the current Auth0 role. Users + // with no role are denied in auth.ts before a session exists. + // `role` is our additional field; the type only lists core fields. + mapProfileToUser: (profile) => { + const role = resolveAuth0Role(profile, profile.email ?? "", config); + return (role ? { role } : {}) as Record; + }, + }, + ], + }); +} + +/** + * Map an Auth0 user's ID-token claims to a CodeRunner role. Admin emails + * (CODERUNNER_ADMIN_EMAIL) are always admin; otherwise the roles claim must + * hold the configured admin or user role name. Null means "no access". + */ +export function resolveAuth0Role( + claims: Record, + email: string, + config: ControlConfig, +): "admin" | "student" | null { + if (config.adminEmails.includes(email.toLowerCase())) { + return "admin"; + } + const raw = claims[config.auth0RolesClaim]; + const roles = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []; + if (roles.includes(config.auth0AdminRoleName)) { + return "admin"; + } + if (roles.includes(config.auth0UserRoleName)) { + return "student"; + } + return null; +} diff --git a/apps/control/src/config.ts b/apps/control/src/config.ts index 72d458e3..71c8c166 100644 --- a/apps/control/src/config.ts +++ b/apps/control/src/config.ts @@ -24,6 +24,15 @@ export type ControlConfig = { githubClientSecret: string | null; googleClientId: string | null; googleClientSecret: string | null; + auth0Domain: string | null; + auth0ClientId: string | null; + auth0ClientSecret: string | null; + /** ID-token claim holding the user's Auth0 role names. */ + auth0RolesClaim: string; + /** Auth0 role name that maps to CodeRunner `student`. */ + auth0UserRoleName: string; + /** Auth0 role name that maps to CodeRunner `admin`. */ + auth0AdminRoleName: string; dockerPath: string; codeImage: string; codeMemoryLimit: string; @@ -334,6 +343,18 @@ export function loadControlConfig( googleClientId: input.googleClientId ?? Bun.env.GOOGLE_CLIENT_ID ?? null, googleClientSecret: input.googleClientSecret ?? Bun.env.GOOGLE_CLIENT_SECRET ?? null, + auth0Domain: input.auth0Domain ?? Bun.env.AUTH0_DOMAIN ?? null, + auth0ClientId: input.auth0ClientId ?? Bun.env.AUTH0_CLIENT_ID ?? null, + auth0ClientSecret: + input.auth0ClientSecret ?? Bun.env.AUTH0_CLIENT_SECRET ?? null, + auth0RolesClaim: + input.auth0RolesClaim ?? + Bun.env.AUTH0_ROLES_CLAIM ?? + "https://coderunner/roles", + auth0UserRoleName: + input.auth0UserRoleName ?? Bun.env.AUTH0_USER_ROLE_NAME ?? "user", + auth0AdminRoleName: + input.auth0AdminRoleName ?? Bun.env.AUTH0_ADMIN_ROLE_NAME ?? "admin", dockerPath: input.dockerPath ?? Bun.env.FRC_DOCKER_PATH ?? "docker", codeImage: input.codeImage ?? diff --git a/apps/control/src/storage.ts b/apps/control/src/storage.ts index f56a842c..1e0c6de7 100644 --- a/apps/control/src/storage.ts +++ b/apps/control/src/storage.ts @@ -177,15 +177,21 @@ export class AppStorage { const hasGoogle = Boolean( this.config.googleClientId && this.config.googleClientSecret, ); - if (!hasGitHub && !hasGoogle) { + const hasAuth0 = Boolean( + this.config.auth0Domain && + this.config.auth0ClientId && + this.config.auth0ClientSecret, + ); + if (!hasGitHub && !hasGoogle && !hasAuth0) { log.warn("no OAuth providers configured — login will not work", { baseUrl: this.config.baseUrl, - hint: "Set GITHUB_CLIENT_ID/SECRET or GOOGLE_CLIENT_ID/SECRET in your .env", + hint: "Set GITHUB_CLIENT_ID/SECRET, GOOGLE_CLIENT_ID/SECRET, or AUTH0_DOMAIN/CLIENT_ID/CLIENT_SECRET in your .env", }); } else { log.debug("oauth providers configured", { github: hasGitHub, google: hasGoogle, + auth0: hasAuth0, }); } } diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index 2a76f966..0b2ca91a 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,5 +1,7 @@ +import { genericOAuthClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ baseURL: window.location.origin, + plugins: [genericOAuthClient()], }); diff --git a/apps/web/src/routes/LoginPage.tsx b/apps/web/src/routes/LoginPage.tsx index f775cd47..a1a1dd1f 100644 --- a/apps/web/src/routes/LoginPage.tsx +++ b/apps/web/src/routes/LoginPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; -import { SiGithub, SiGoogle } from "react-icons/si"; +import type { IconType } from "react-icons"; +import { SiAuth0, SiGithub, SiGoogle } from "react-icons/si"; import { useSearchParams } from "react-router"; import coderunnerMascotImg from "@/assets/coderunner-mascot.png"; import { authClient } from "@/lib/auth-client"; @@ -9,14 +10,38 @@ import { } from "@/lib/contracts"; import { cn } from "@/lib/utils"; +const PROVIDER_BUTTONS: Record< + AuthProvider, + { label: string; Icon: IconType; className: string } +> = { + github: { + label: "Sign in with GitHub", + Icon: SiGithub, + className: + "border-border bg-white/[0.06] text-foreground hover:bg-white/[0.11] hover:shadow-[0_0_18px_rgba(34,197,94,0.12)]", + }, + google: { + label: "Sign in with Google", + Icon: SiGoogle, + className: + "border-border bg-white/[0.03] text-foreground hover:bg-white/[0.06]", + }, + auth0: { + label: "Sign in with Auth0", + Icon: SiAuth0, + className: + "border-border bg-white/[0.03] text-foreground hover:bg-white/[0.06]", + }, +}; + interface OAuthButtonProps { - provider: "github" | "google"; + provider: AuthProvider; disabled: boolean; onClick: () => void; } function OAuthButton({ provider, disabled, onClick }: OAuthButtonProps) { - const isGitHub = provider === "github"; + const { label, Icon, className } = PROVIDER_BUTTONS[provider]; return (
); @@ -90,15 +109,24 @@ export function LoginPage() { }; }, []); - async function signIn(provider: "github" | "google") { + async function signIn(provider: AuthProvider) { setLoading(true); setSignInError(null); try { - await authClient.signIn.social({ - provider, - callbackURL: "/", - errorCallbackURL: "/login", - }); + if (provider === "auth0") { + // Auth0 is a genericOAuth provider, not a Better Auth social one. + await authClient.signIn.oauth2({ + providerId: "auth0", + callbackURL: "/", + errorCallbackURL: "/login", + }); + } else { + await authClient.signIn.social({ + provider, + callbackURL: "/", + errorCallbackURL: "/login", + }); + } } catch (error) { setLoading(false); setSignInError( diff --git a/docs/about/security-model.md b/docs/about/security-model.md index 83b41969..13921ad8 100644 --- a/docs/about/security-model.md +++ b/docs/about/security-model.md @@ -31,6 +31,14 @@ user's identity, CodeRunner checks whether the returned email address is on the user creation and again on every OAuth callback), so a removed entry takes effect at the next login attempt. +**Auth0 sign-ins skip the allowlist.** When Auth0 is configured, the operator's +Auth0 tenant is trusted to decide who gets in: CodeRunner reads a roles claim +from the Auth0 ID token at every Auth0 sign-in and admits the user only if it +holds the configured user or admin role (the admin role grants CodeRunner +admin). A user whose roles are removed in Auth0 is denied at their next sign-in, +before any session is issued; sessions they already hold run until they expire. +See [Set up Auth0](../deploying/oauth-credentials.md#set-up-auth0). + The allowlist accepts individual addresses and whole domains. A team using `@frcteam1234.org` Google Workspace accounts can add that domain once rather than listing every member. The file format is: diff --git a/docs/decisions/044-auth0-sso.md b/docs/decisions/044-auth0-sso.md new file mode 100644 index 00000000..493b0c38 --- /dev/null +++ b/docs/decisions/044-auth0-sso.md @@ -0,0 +1,55 @@ +# 044 — Auth0 SSO + +## Decision + +Add Auth0 as a third sign-in provider (gh #24) for teams that already manage +members in an Auth0 tenant. Auth0 is enabled when `AUTH0_DOMAIN`, +`AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` are set, and sits alongside GitHub +and Google; each configured provider gets a login button. + +Auth0 is not a Better Auth social provider, so it goes through the +`genericOAuth` plugin's `auth0()` helper (OIDC discovery). Its callback is +`/api/auth/oauth2/callback/auth0`, not `/callback/:id`, which is how the hooks +tell Auth0 sign-ins apart. + +**Auth0 replaces the allowlist for its users.** The operator's tenant is +trusted to vet members. Authorization comes from a roles claim in the ID token, +put there by a Post-Login Action (`api.idToken.setCustomClaim`), rather than +from RBAC permissions in an access token: the ID token is what Better Auth +already decodes, and an Action needs no Auth0 API/audience setup. The claim +name and the two role names are env vars (`AUTH0_ROLES_CLAIM`, +`AUTH0_USER_ROLE_NAME`, `AUTH0_ADMIN_ROLE_NAME`; defaults +`https://coderunner/roles`, `user`, `admin`) so a tenant with existing role +names (e.g. `teacher`) only has to match them. + +- The admin role maps to `admin`, the user role to `student`; neither means + denied. `CODERUNNER_ADMIN_EMAIL` still grants admin and skips the role + requirement, so the operator's bootstrap admin works whatever the tenant does. +- Roles are re-read at every Auth0 sign-in. `mapProfileToUser` returns the role + and `overrideUserInfo` writes it to the user row before the session is made, + so the session cookie cache carries the current role. An admin-panel role + change on an Auth0 user is overwritten at their next sign-in. +- New users without a role are rejected in `user.create.before` (no user row). + Returning users without a role are rejected in `session.create.before`, which + re-reads the role from the ID token Better Auth just stored on the account. + +**Why not deny in the after hook,** as the allowlist does for returning social +users: Better Auth (1.6.10) keeps the callback's original 302 status and +`Location` when an after hook throws, so the error is lost and the browser is +sent to `/` with fresh session and `session_data` cookies. Deleting the session +row does not help because `getSession` trusts the 5-minute cookie cache. Errors +from session creation are not caught by the callback, so a redirect thrown +there reaches the browser as `/login?error=…` with no session at all. + +The returning-user allowlist check for GitHub/Google has the same after-hook +weakness; it is out of scope here and left unchanged. + +## Validation + +`apps/control/src/__tests__/auth0.test.ts` runs the real Better Auth sign-in and +callback against a stubbed Auth0 (discovery + token endpoint via `fetch`): +student and admin first sign-in without an allowlist entry, no-role denial +without a user row, the admin-email override, promotion/demotion on later +sign-ins (DB and session cookie), and a no-role returning user getting +`/login?error=…` with no new session. Role mapping and provider discovery have +unit tests. A real Auth0 tenant round trip has not been run. diff --git a/docs/deploying/oauth-credentials.md b/docs/deploying/oauth-credentials.md index b526cccf..d82050c1 100644 --- a/docs/deploying/oauth-credentials.md +++ b/docs/deploying/oauth-credentials.md @@ -6,12 +6,14 @@ title: OAuth Credentials # OAuth Credentials CodeRunner does not store passwords. Sign-in is handled by -[Better Auth](https://www.better-auth.com/) using GitHub and/or Google as OAuth -providers. **At least one provider must be configured** for any non-demo -deployment; without one, the login page has no working sign-in button. +[Better Auth](https://www.better-auth.com/) using GitHub, Google, and/or +Auth0 as OAuth providers. **At least one provider must be configured** for any +non-demo deployment; without one, the login page has no working sign-in button. -You only need both if you want students to choose between GitHub and Google; -configuring one is fine. +Configure as many as you like; each configured provider gets its own button. +Auth0 is for teams that already manage their members in an Auth0 tenant: Auth0 +users skip the allowlist and get their CodeRunner role from Auth0 (see +[Set up Auth0](#set-up-auth0)). This page covers registering the OAuth apps and wiring the resulting credentials into CodeRunner. The values you produce here are used the same way @@ -33,6 +35,7 @@ Every OAuth app registration asks for a homepage/origin URL and a redirect | --- | --- | | GitHub | `/api/auth/callback/github` | | Google | `/api/auth/callback/google` | + | Auth0 | `/api/auth/oauth2/callback/auth0` | For local development that is `http://localhost:4000/api/auth/callback/github` and `.../google`. For the cloud VM it is @@ -63,6 +66,54 @@ In the Google Cloud console: You now have a **Client ID** and a **Client Secret**. +## Set up Auth0 + +Auth0 sign-in works differently from GitHub and Google: Auth0 has already +vetted your members, so **Auth0 users skip the allowlist**. Instead, CodeRunner +reads the user's Auth0 roles at every sign-in: + +| Auth0 roles | CodeRunner result | +| --- | --- | +| Has the admin role (`AUTH0_ADMIN_ROLE_NAME`, default `admin`) | Signs in as an admin | +| Has the user role (`AUTH0_USER_ROLE_NAME`, default `user`) | Signs in as a student | +| Neither | Denied | + +An email listed in `CODERUNNER_ADMIN_EMAIL` always signs in as an admin, with or +without Auth0 roles. Because roles are re-read at every Auth0 sign-in, changing a +user's roles in Auth0 takes effect the next time they sign in. Promoting or +demoting an Auth0 user in the CodeRunner admin panel is overwritten at that +user's next sign-in, so manage Auth0 users' roles in Auth0. + +In the Auth0 dashboard: + +1. **Applications → Applications → Create Application**, type **Regular Web + Application**. In its **Settings**, set **Allowed Callback URLs** to + `/api/auth/oauth2/callback/auth0`. Note the **Domain**, + **Client ID**, and **Client Secret**. +2. **User Management → Roles**: create the two roles (e.g. `user` and `admin`) + and assign them to your members. If your tenant already has roles with other + names (e.g. `teacher`), set `AUTH0_USER_ROLE_NAME` / `AUTH0_ADMIN_ROLE_NAME` + to match instead. +3. Auth0 does not put roles in the ID token by default. **Actions → Library → + Create Action → Build from scratch**, trigger **Login / Post Login**, with: + + ```js + exports.onExecutePostLogin = async (event, api) => { + api.idToken.setCustomClaim( + "https://coderunner/roles", + event.authorization?.roles ?? [], + ); + }; + ``` + + **Deploy** it, then add it to the flow under **Actions → Triggers → + post-login**. The claim name is only a label inside the token (Auth0's + convention is a URL-shaped namespace; nothing fetches it). If you use a + different name, set `AUTH0_ROLES_CLAIM` to match. + +Then set `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` (below). +The login page shows **Sign in with Auth0** once all three are set. + ## Wire the credentials into CodeRunner CodeRunner reads these from environment variables (see @@ -76,9 +127,15 @@ CodeRunner reads these from environment variables (see | `GITHUB_CLIENT_SECRET` | GitHub OAuth app client secret | | `GOOGLE_CLIENT_ID` | Google OAuth client ID | | `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | +| `AUTH0_DOMAIN` | Auth0 tenant domain, e.g. `myteam.us.auth0.com` | +| `AUTH0_CLIENT_ID` | Auth0 application client ID | +| `AUTH0_CLIENT_SECRET` | Auth0 application client secret | +| `AUTH0_ROLES_CLAIM` | ID-token claim with the role names (default `https://coderunner/roles`) | +| `AUTH0_USER_ROLE_NAME` | Auth0 role that signs in as a student (default `user`) | +| `AUTH0_ADMIN_ROLE_NAME` | Auth0 role that signs in as an admin (default `admin`) | A provider only appears on the login page when **both** its ID and secret are -set. Where these values live depends on the deployment: +set (for Auth0, the domain too). Where these values live depends on the deployment: - **Local:** in your `.env` file. See [Local Deployment](./local.md). - **Cloud VM:** in Google Secret Manager, materialized into the VM's `.env` by @@ -88,6 +145,9 @@ set. Where these values live depends on the deployment: OAuth establishes *who* a person is; CodeRunner separately controls *whether* they may sign in (the allowlist) and *whether* they are an admin (the role). +This section applies to GitHub and Google sign-ins; Auth0 users get both from +their Auth0 roles instead (see [Set up Auth0](#set-up-auth0)), though +`CODERUNNER_ADMIN_EMAIL` still makes them an admin. ### The easy path: `CODERUNNER_ADMIN_EMAIL` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 3f8be4d6..ba43bb9f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -54,7 +54,7 @@ See [Lessons overview](../lessons/overview.md) and [Authoring modules](../lesson ## Auth and OAuth -OAuth credentials are required for multi-user production deployments. At least one provider (GitHub or Google) must be configured for login to work. Register your OAuth app at the provider and set the callback URL to `$BETTER_AUTH_URL/api/auth/callback/github` or `$BETTER_AUTH_URL/api/auth/callback/google`. +OAuth credentials are required for multi-user production deployments. At least one provider (GitHub, Google, or Auth0) must be configured for login to work. Register your OAuth app at the provider and set the callback URL to `$BETTER_AUTH_URL/api/auth/callback/github`, `$BETTER_AUTH_URL/api/auth/callback/google`, or `$BETTER_AUTH_URL/api/auth/oauth2/callback/auth0`. See [OAuth credentials](../deploying/oauth-credentials.md) for step-by-step registration instructions. @@ -66,6 +66,12 @@ See [OAuth credentials](../deploying/oauth-credentials.md) for step-by-step regi | `GITHUB_CLIENT_SECRET` | none | GitHub OAuth app client secret. | | `GOOGLE_CLIENT_ID` | none | Google OAuth client ID. | | `GOOGLE_CLIENT_SECRET` | none | Google OAuth client secret. | +| `AUTH0_DOMAIN` | none | Auth0 tenant domain (e.g. `myteam.us.auth0.com`). Auth0 sign-in is enabled when this, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` are all set. | +| `AUTH0_CLIENT_ID` | none | Auth0 application client ID. | +| `AUTH0_CLIENT_SECRET` | none | Auth0 application client secret. | +| `AUTH0_ROLES_CLAIM` | `https://coderunner/roles` | ID-token claim that holds the user's Auth0 role names. Auth0 users skip the allowlist; this claim decides whether they get in. See [Set up Auth0](../deploying/oauth-credentials.md#set-up-auth0). | +| `AUTH0_USER_ROLE_NAME` | `user` | Auth0 role name that signs in as a CodeRunner student. | +| `AUTH0_ADMIN_ROLE_NAME` | `admin` | Auth0 role name that signs in as a CodeRunner admin. | | `CODERUNNER_DEMO_MODE` | `false` | When `1` or `true`, bypasses authentication entirely. All visitors share one admin session. Never expose a demo instance publicly. Also enabled with the `--demo` CLI flag on startup. | | `CODERUNNER_ADMIN_EMAIL` | none | Comma-separated email addresses to bootstrap as admins with zero exec steps. Each is added to the allowlist at startup and granted the admin role on first OAuth sign-in; an existing account with that email is promoted to admin at the next startup. See [OAuth credentials](../deploying/oauth-credentials.md#the-easy-path-coderunner_admin_email). | diff --git a/packages/contracts/src/index.test.ts b/packages/contracts/src/index.test.ts index 5e573308..55704476 100644 --- a/packages/contracts/src/index.test.ts +++ b/packages/contracts/src/index.test.ts @@ -60,6 +60,11 @@ describe("simulation API schemas", () => { ).toEqual({ providers: ["github"], }); + expect(authProvidersResponseSchema.parse({ providers: ["auth0"] })).toEqual( + { + providers: ["auth0"], + }, + ); expect( authProvidersResponseSchema.safeParse({ providers: ["discord"] }).success, ).toBe(false); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 41b54c57..2c22bf8a 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -60,7 +60,7 @@ export const sessionResponseSchema = z.object({ demo: z.boolean().optional(), }); -export const authProviderSchema = z.enum(["github", "google"]); +export const authProviderSchema = z.enum(["github", "google", "auth0"]); export const authProvidersResponseSchema = z.object({ providers: z.array(authProviderSchema), From b80814d2d638ef99da7c42680310172a1ffbb802 Mon Sep 17 00:00:00 2001 From: Mathew Dunne Date: Thu, 1 Oct 2026 12:57:13 -0400 Subject: [PATCH 2/4] review feedback fixes --- AGENTS.md | 9 +- apps/control/src/__tests__/auth0.test.ts | 36 +- apps/control/src/auth/auth.ts | 87 +- apps/control/src/auth/providers.ts | 39 +- docs/decisions/044-auth0-sso.md | 25 +- docs/deploying/oauth-credentials.md | 8 +- graphify-out/GRAPH_REPORT.md | 878 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 9358 ++++++++++++---------- 9 files changed, 5544 insertions(+), 4904 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6cf3d55c..3f070279 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,10 +94,11 @@ through Better Auth's `genericOAuth` plugin (callback `/api/auth/oauth2/callback/auth0`). Auth0 users skip the allowlist; a roles claim in the ID token (`AUTH0_ROLES_CLAIM`, set by a Post-Login Action) maps `AUTH0_ADMIN_ROLE_NAME` → admin and `AUTH0_USER_ROLE_NAME` → student, and is -re-read at every sign-in. No role means denied, enforced in -`user.create.before` (new) and `session.create.before` (returning) — not the -after hook, whose throws Better Auth loses behind the callback redirect. -`CODERUNNER_ADMIN_EMAIL` still grants admin. See +re-read at every sign-in. No role means denied, enforced in the Auth0 +`mapProfileToUser` (it sees exactly the identity signing in, before Better Auth +creates, links, or updates anything) by throwing a redirect — not the after +hook, whose throws Better Auth loses behind the callback redirect. +`CODERUNNER_ADMIN_EMAIL` still grants admin, but only with `email_verified`. See `docs/decisions/044-auth0-sso.md` and [`docs/deploying/oauth-credentials.md`](./docs/deploying/oauth-credentials.md). diff --git a/apps/control/src/__tests__/auth0.test.ts b/apps/control/src/__tests__/auth0.test.ts index 097cea6a..efae98ca 100644 --- a/apps/control/src/__tests__/auth0.test.ts +++ b/apps/control/src/__tests__/auth0.test.ts @@ -178,8 +178,23 @@ describe("resolveAuth0Role", () => { ).toBeNull(); }); - test("admin emails are admin regardless of roles", () => { - expect(resolveAuth0Role({}, "Coach@Test.local", config)).toBe("admin"); + test("verified admin emails are admin regardless of roles", () => { + expect( + resolveAuth0Role({ email_verified: true }, "Coach@Test.local", config), + ).toBe("admin"); + }); + + test("unverified admin emails fall back to the roles claim", () => { + expect( + resolveAuth0Role({ email_verified: false }, "coach@test.local", config), + ).toBeNull(); + expect( + resolveAuth0Role( + { email_verified: false, [ROLES_CLAIM]: ["user"] }, + "coach@test.local", + config, + ), + ).toBe("student"); }); test("honours custom claim and role names", () => { @@ -288,4 +303,21 @@ describe("Auth0 sign-in", () => { expect(await sessionRole(app, denied)).toBeNull(); }, auth0Options); }); + + test("each linked Auth0 identity is checked on its own roles", async () => { + await withApp(async (app) => { + const email = "dual@team.test"; + expectSignedIn( + await auth0Login(app, auth0User("auth0|dual", email, ["admin"])), + ); + // Same verified email, different Auth0 identity, no role: denied, + // not waved through on the first identity's admin role. + const denied = await auth0Login( + app, + auth0User("google-oauth2|dual", email), + ); + expect(denied.headers.get("location")).toContain("/login?error="); + expect(await sessionRole(app, denied)).toBeNull(); + }, auth0Options); + }); }); diff --git a/apps/control/src/auth/auth.ts b/apps/control/src/auth/auth.ts index 920e41e1..36997abd 100644 --- a/apps/control/src/auth/auth.ts +++ b/apps/control/src/auth/auth.ts @@ -17,18 +17,12 @@ import type { ControlConfig } from "../config"; import { getLogger } from "../logging"; import { isEmailAllowed, reloadAllowlist } from "./allowlist"; import { classroomPlugin } from "./classroom-plugin"; -import { - buildAuth0Plugin, - buildSocialProviders, - resolveAuth0Role, -} from "./providers"; +import { buildAuth0Plugin, buildSocialProviders } from "./providers"; const log = getLogger("auth"); const ROSTER_MESSAGE = "Your email is not on the roster. Ask your coach to add you."; -const AUTH0_NO_ROLE_MESSAGE = - "Your Auth0 account has no CodeRunner role. Ask your coach for access."; /** True for the genericOAuth callback of the Auth0 provider. */ function isAuth0Callback( @@ -46,47 +40,6 @@ function isAuth0Callback( ); } -/** - * Read an ID token's payload. No signature check: Better Auth received the - * token directly from Auth0's token endpoint and trusts it the same way. - */ -function decodeIdTokenClaims(idToken: string): Record { - try { - const payload = idToken.split(".")[1] ?? ""; - const claims = JSON.parse(Buffer.from(payload, "base64url").toString()); - return claims && typeof claims === "object" ? claims : {}; - } catch { - return {}; - } -} - -type Auth0Adapter = { - findUserById(userId: string): Promise<{ email: string } | null>; - findAccounts( - userId: string, - ): Promise<{ providerId: string; idToken?: string | null | undefined }[]>; -}; - -/** - * The role carried by the ID token Better Auth stored for the user's Auth0 - * account on this sign-in (tokens are refreshed before the session is made). - */ -async function currentAuth0Role( - adapter: Auth0Adapter, - userId: string, - config: ControlConfig, -): Promise<"admin" | "student" | null> { - const [user, accounts] = await Promise.all([ - adapter.findUserById(userId), - adapter.findAccounts(userId), - ]); - const idToken = accounts.find((a) => a.providerId === "auth0")?.idToken; - if (!user || !idToken) { - return null; - } - return resolveAuth0Role(decodeIdTokenClaims(idToken), user.email, config); -} - /** * Reload allowlist.json from disk before enforcing it, so CLI edits * (`coderunner allowlist add`) take effect on the next sign-in attempt @@ -207,16 +160,9 @@ export function createAuth( const slug = slugFromEmail(user.email); if (isAuth0Callback(ctx)) { // Auth0 vets the user; the role claim replaces the allowlist. - // genericOAuth spreads the ID-token claims onto `user`. - const role = resolveAuth0Role(user, user.email, config); - if (!role) { - log.warn("new auth0 user rejected: no role", { - email: user.email, - }); - throw new APIError("FORBIDDEN", { - message: AUTH0_NO_ROLE_MESSAGE, - }); - } + // The Auth0 profile mapper already denied users with no role + // and put the role on `user`. + const role = (user as { role?: string }).role; log.info("creating new auth0 user", { email: user.email, slug, @@ -248,31 +194,6 @@ export function createAuth( }, }, }, - session: { - create: { - before: async (session, ctx) => { - if (!ctx || !isAuth0Callback(ctx)) { - return; - } - // Every Auth0 sign-in re-checks the role claim. Deny here, before - // any session or cookie exists: Better Auth doesn't catch errors - // from session creation, and an after-hook throw would be lost - // behind the callback's redirect. - const role = await currentAuth0Role( - ctx.context.internalAdapter, - session.userId, - config, - ); - if (!role) { - log.warn("auth0 sign-in rejected: no role", { - userId: session.userId, - }); - const error = AUTH0_NO_ROLE_MESSAGE.replaceAll(" ", "_"); - throw ctx.redirect(`/login?error=${encodeURIComponent(error)}`); - } - }, - }, - }, }, hooks: { after: createAuthMiddleware(async (ctx) => { diff --git a/apps/control/src/auth/providers.ts b/apps/control/src/auth/providers.ts index 8541d7e5..df641e9e 100644 --- a/apps/control/src/auth/providers.ts +++ b/apps/control/src/auth/providers.ts @@ -5,8 +5,15 @@ * Google are Better Auth social providers; Auth0 goes through the genericOAuth * plugin (decision 044). */ +import { APIError } from "better-auth/api"; import { auth0, genericOAuth } from "better-auth/plugins/generic-oauth"; import type { ControlConfig } from "../config"; +import { getLogger } from "../logging"; + +const log = getLogger("auth"); + +const AUTH0_NO_ROLE_MESSAGE = + "Your Auth0 account has no CodeRunner role. Ask your coach for access."; export type SocialProviders = { github?: { @@ -81,13 +88,25 @@ export function buildAuth0Plugin(config: ControlConfig) { clientSecret: config.auth0ClientSecret!, }), overrideUserInfo: true, - // Write the role on every sign-in so the user row (and the session - // cookie cache built from it) carries the current Auth0 role. Users - // with no role are denied in auth.ts before a session exists. + // Runs once per callback with the claims of the identity signing in, + // before Better Auth creates, links, or updates anything. No role + // means denied here; otherwise the role is written on every sign-in + // so the user row (and the session cookie cache built from it) + // carries the current Auth0 role. // `role` is our additional field; the type only lists core fields. mapProfileToUser: (profile) => { const role = resolveAuth0Role(profile, profile.email ?? "", config); - return (role ? { role } : {}) as Record; + if (!role) { + log.warn("auth0 sign-in rejected: no role", { + email: profile.email, + }); + const error = AUTH0_NO_ROLE_MESSAGE.replaceAll(" ", "_"); + // What ctx.redirect throws; the profile mapper has no ctx. + throw new APIError("FOUND", undefined, { + location: `/login?error=${encodeURIComponent(error)}`, + }); + } + return { role } as Record; }, }, ], @@ -96,15 +115,21 @@ export function buildAuth0Plugin(config: ControlConfig) { /** * Map an Auth0 user's ID-token claims to a CodeRunner role. Admin emails - * (CODERUNNER_ADMIN_EMAIL) are always admin; otherwise the roles claim must - * hold the configured admin or user role name. Null means "no access". + * (CODERUNNER_ADMIN_EMAIL) are admin once Auth0 has verified the address; + * otherwise the roles claim must hold the configured admin or user role + * name. Null means "no access". */ export function resolveAuth0Role( claims: Record, email: string, config: ControlConfig, ): "admin" | "student" | null { - if (config.adminEmails.includes(email.toLowerCase())) { + // An unverified address proves nothing: anyone can register it in a + // tenant that allows sign-up. + if ( + claims.email_verified === true && + config.adminEmails.includes(email.toLowerCase()) + ) { return "admin"; } const raw = claims[config.auth0RolesClaim]; diff --git a/docs/decisions/044-auth0-sso.md b/docs/decisions/044-auth0-sso.md index 493b0c38..c83d93fb 100644 --- a/docs/decisions/044-auth0-sso.md +++ b/docs/decisions/044-auth0-sso.md @@ -24,21 +24,30 @@ names (e.g. `teacher`) only has to match them. - The admin role maps to `admin`, the user role to `student`; neither means denied. `CODERUNNER_ADMIN_EMAIL` still grants admin and skips the role - requirement, so the operator's bootstrap admin works whatever the tenant does. + requirement, so the operator's bootstrap admin works whatever the tenant does + — but only when the ID token says `email_verified: true`. A tenant that + allows sign-up lets anyone register an address and sign in before verifying + it, so an unverified admin email falls back to the roles claim. - Roles are re-read at every Auth0 sign-in. `mapProfileToUser` returns the role and `overrideUserInfo` writes it to the user row before the session is made, so the session cookie cache carries the current role. An admin-panel role change on an Auth0 user is overwritten at their next sign-in. -- New users without a role are rejected in `user.create.before` (no user row). - Returning users without a role are rejected in `session.create.before`, which - re-reads the role from the ID token Better Auth just stored on the account. +- Users without a role are rejected in `mapProfileToUser`, which throws the + same `APIError("FOUND")` that `ctx.redirect` builds. It runs once per callback + with the claims of the identity signing in, before Better Auth creates, + links, or updates anything, so a denied sign-in leaves no user row, account + row, or session. An earlier version checked in `session.create.before` by + reading the ID token stored on the user's `auth0` account; Auth0 gives each + connection (database, Google, …) its own `sub`, and Better Auth links two + identities with the same verified email as two `auth0` accounts on one user, + so that check could read the other identity's token and its role. **Why not deny in the after hook,** as the allowlist does for returning social users: Better Auth (1.6.10) keeps the callback's original 302 status and `Location` when an after hook throws, so the error is lost and the browser is sent to `/` with fresh session and `session_data` cookies. Deleting the session row does not help because `getSession` trusts the 5-minute cookie cache. Errors -from session creation are not caught by the callback, so a redirect thrown +from the profile mapper are not caught by the callback, so a redirect thrown there reaches the browser as `/login?error=…` with no session at all. The returning-user allowlist check for GitHub/Google has the same after-hook @@ -50,6 +59,8 @@ weakness; it is out of scope here and left unchanged. callback against a stubbed Auth0 (discovery + token endpoint via `fetch`): student and admin first sign-in without an allowlist entry, no-role denial without a user row, the admin-email override, promotion/demotion on later -sign-ins (DB and session cookie), and a no-role returning user getting -`/login?error=…` with no new session. Role mapping and provider discovery have +sign-ins (DB and session cookie), a no-role returning user getting +`/login?error=…` with no new session, and a no-role second identity linked to +an admin user being denied. Unverified admin emails are covered by the role +mapping unit tests. Role mapping and provider discovery have unit tests. A real Auth0 tenant round trip has not been run. diff --git a/docs/deploying/oauth-credentials.md b/docs/deploying/oauth-credentials.md index d82050c1..08fbaf68 100644 --- a/docs/deploying/oauth-credentials.md +++ b/docs/deploying/oauth-credentials.md @@ -78,8 +78,9 @@ reads the user's Auth0 roles at every sign-in: | Has the user role (`AUTH0_USER_ROLE_NAME`, default `user`) | Signs in as a student | | Neither | Denied | -An email listed in `CODERUNNER_ADMIN_EMAIL` always signs in as an admin, with or -without Auth0 roles. Because roles are re-read at every Auth0 sign-in, changing a +An email listed in `CODERUNNER_ADMIN_EMAIL` signs in as an admin, with or without +Auth0 roles, once Auth0 has verified that address (`email_verified`). An +unverified address gets no special treatment and needs a role like anyone else. Because roles are re-read at every Auth0 sign-in, changing a user's roles in Auth0 takes effect the next time they sign in. Promoting or demoting an Auth0 user in the CodeRunner admin panel is overwritten at that user's next sign-in, so manage Auth0 users' roles in Auth0. @@ -147,7 +148,8 @@ OAuth establishes *who* a person is; CodeRunner separately controls *whether* they may sign in (the allowlist) and *whether* they are an admin (the role). This section applies to GitHub and Google sign-ins; Auth0 users get both from their Auth0 roles instead (see [Set up Auth0](#set-up-auth0)), though -`CODERUNNER_ADMIN_EMAIL` still makes them an admin. +`CODERUNNER_ADMIN_EMAIL` still makes them an admin if Auth0 has verified the +address. ### The easy path: `CODERUNNER_ADMIN_EMAIL` diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index b95eedc4..40a312b8 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,16 +1,16 @@ -# Graph Report - CodeRunner (2026-09-30) +# Graph Report - CodeRunner (2026-10-01) ## Corpus Check -- 385 files · ~318,348 words +- 387 files · ~320,746 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 4648 nodes · 6821 edges · 383 communities (293 shown, 90 thin omitted) +- 4675 nodes · 6859 edges · 383 communities (296 shown, 87 thin omitted) - Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 222 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Graph Freshness -- Built from commit: `fb6451fd` +- Built from commit: `c95caa40` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). @@ -393,12 +393,12 @@ 2. `AppStorage` - 47 edges 3. `LocalDockerRuntimeProvider` - 40 edges 4. `test` - 37 edges -5. `loginAs()` - 35 edges -6. `withApp()` - 35 edges +5. `withApp()` - 37 edges +6. `loginAs()` - 35 edges 7. `LocalDockerRuntimeProvider` - 33 edges 8. `RunManager` - 29 edges -9. `login()` - 28 edges -10. `getLogger()` - 27 edges +9. `getLogger()` - 28 edges +10. `login()` - 28 edges ## Surprising Connections (you probably didn't know these) - `nt4-multi-workspace.spec.ts (T35.1 NT4 isolation test)` --semantically_similar_to--> `embedded-mode NT4 endpoint injection mechanism` [INFERRED] [semantically similar] @@ -417,555 +417,559 @@ - **In-Process Fake Upstream Servers for Mocked E2E Tier** — testing_fake_vscode, testing_fake_halsim, testing_fake_nt4, testing_controlapp [EXTRACTED 1.00] - **VSCodium/Java Tooling Decision Chain** — decisions_readme_036_vscodium_web_migration, decisions_readme_037_gradle_wrapper_alias_and_extension_pins, decisions_readme_038_java_tooling_compatibility_and_smoke [EXTRACTED 1.00] -## Communities (383 total, 90 thin omitted) +## Communities (383 total, 87 thin omitted) ### Community 0 - "Admin API Contracts" Cohesion: 0.03 Nodes (78): AdminActionResponse, adminActionResponseSchema, AdminStatusResponse, adminStatusResponseSchema, AdminWorkspaceStatus, adminWorkspaceStatusSchema, AllianceStation, AuthProvider (+70 more) ### Community 1 - "Admin Route Dispatch & Security Headers" -Cohesion: 0.05 -Nodes (62): AdminClassroomContext, ClassroomJoinContext, classroomView(), clientIp(), handleAdminClassroomRoute(), handleClassroomJoin(), log, retirePreviousSession() (+54 more) +Cohesion: 0.04 +Nodes (49): DemoBanner(), EditorPane(), EditorPaneProps, LayoutMenu(), ScopePane, EditorReachability, EditorStatus, { result } (+41 more) ### Community 2 - "App Storage (SQLite)" -Cohesion: 0.05 -Nodes (41): sendUpstreamWebSocketMessage(), AppSocket, log, WebSocketHandlerContext, first, NUM_RUNS, second, cloneCall (+33 more) +Cohesion: 0.06 +Nodes (54): AdminClassroomContext, ClassroomJoinContext, classroomView(), clientIp(), handleAdminClassroomRoute(), handleClassroomJoin(), log, retirePreviousSession() (+46 more) ### Community 3 - "IDE Layout Resizable Panes" -Cohesion: 0.08 -Nodes (36): AdminApp(), AdminLayout(), Tab, tabs, useAdminPoll(), Allowlist(), AllowlistData, AuditEntry (+28 more) +Cohesion: 0.06 +Nodes (44): Avatar(), formatEndsAt(), leaveClassroom(), signOut(), UserMenu(), UserMenuProps, CodeStatus, CodeStatusPill() (+36 more) ### Community 4 - "WebSocket Handler Factory" -Cohesion: 0.06 -Nodes (47): Args, backupDatabase(), dirExists(), fileExists(), main(), parseArgs(), runTar(), timestamp() (+39 more) +Cohesion: 0.05 +Nodes (64): handleAdminRoute, Admin workspace backup action, Admin workspace restore action, applySecurityHeaders, authorizeMetrics, constantTimeEqual, dispatch, fetch (request handler) (+56 more) ### Community 5 - "Control Plane App & Metrics" -Cohesion: 0.07 -Nodes (28): addAllowlistEntry(), allowlist.spec.ts (auth), e2e/fixtures/app.ts fixture (ControlApp harness), createApp(), Commits 066141e / 1a4f5e6 (Vite base path revert), loginAs() / cookieHeader() auth fixture, ControlApp in-process fixture / createApp(), Decision 015 — workspace routing / DS sync (+20 more) +Cohesion: 0.05 +Nodes (47): createWebSocketHandlers(), getDemoSession(), getDemoSessionResponseBody(), seedDemoUser(), input, stripped, authorizeMetrics(), bootLog (+39 more) ### Community 6 - "Gamepad/Keyboard WPILib Mapping" -Cohesion: 0.04 -Nodes (43): code:ts (describe("deployFilePathSchema", () => {), code:ts (pathplannerDistDir: string;), code:ts (export async function createPathPlannerDist(root: string): P), code:ts (const pathplannerDistDir = await createPathPlannerDist(root)), code:bash (bun run check:fix), code:ts (test("templates PathPlanner routes with bounded cardinality"), code:ts (if (path.startsWith("/pathplanner/")) return "/pathplanner/*), code:ts (if (suffix.startsWith("/api/deploy-files/"))) (+35 more) +Cohesion: 0.08 +Nodes (36): AdminApp(), AdminLayout(), Tab, tabs, useAdminPoll(), Allowlist(), AllowlistData, AuditEntry (+28 more) ### Community 7 - "Admin Layout & Polling" Cohesion: 0.05 -Nodes (47): Web Contracts Re-export, dpadToPov (gamepad-mapping), gamepadFrameToWpilib, WPILib XboxController Axis/Button Layout, dpadToPov (keyboard-mapping), gamepadStateToVisualizerFrame, Keyboard-as-Virtual-Gamepad Emulation, KEYBOARD_BINDINGS (+39 more) +Nodes (39): AllianceToggle(), AllianceToggleProps, Side, SIDE_ACTIVE, SIDE_EDGE, SIDE_STATION, sideOf(), onSelect (+31 more) ### Community 8 - "Shared UI Primitives" Cohesion: 0.05 -Nodes (25): ResolvedTheme, Theme, THEME_VALUES, ThemeProvider(), ThemeProviderContext, ThemeProviderProps, ThemeProviderState, useTheme() (+17 more) +Nodes (35): BINDING_GROUPS, Bumper(), ControlsPanel(), ControlsPanelProps, DpadArm(), FaceButton(), GamepadVisualizer(), SmallButton() (+27 more) ### Community 9 - "Docker Runtime Provider Core" Cohesion: 0.07 -Nodes (24): chooserRoots(), decodeMsgPack(), displayKey(), encodeMsgPack(), isChooser(), normalizeTopicName(), Nt4AutoChooserBridge, Nt4AutoChooserBridgeOptions (+16 more) +Nodes (22): addAllowlistEntry(), AllowlistData, EMPTY, getAllowlist(), isEmailAllowed(), loadAllowlist(), normalize(), normalizeEntry() (+14 more) ### Community 10 - "Run Execution Metrics" -Cohesion: 0.06 -Nodes (35): ADMIN_GET_ROUTES, adminCookie, cookie, req, studentCookie, ManagedWorkspaceRuntime, WorkspaceRuntimeCommand, adminCookie() (+27 more) +Cohesion: 0.05 +Nodes (38): ADMIN_GET_ROUTES, adminCookie, cookie, req, studentCookie, adminCookie(), allCookiesFrom(), cookieFrom() (+30 more) ### Community 11 - "Driver Station Enable/Disable UI" Cohesion: 0.05 -Nodes (45): listWorkspaceDiskLimitDevices, parseDockerStatsLine, runtimeFromLease, statusFromLease, upstreamEndpoints, dockerPortBindError, inspectContainer, inspectContainers (+37 more) +Nodes (27): ResolvedTheme, Theme, THEME_VALUES, ThemeProvider(), ThemeProviderContext, ThemeProviderProps, ThemeProviderState, useTheme() (+19 more) ### Community 12 - "Docker Runtime Types & Ports" -Cohesion: 0.09 -Nodes (25): BINDING_GROUPS, Bumper(), ControlsPanelProps, DpadArm(), FaceButton(), GamepadVisualizer(), SmallButton(), StatusTone (+17 more) +Cohesion: 0.04 +Nodes (43): code:ts (describe("deployFilePathSchema", () => {), code:ts (pathplannerDistDir: string;), code:ts (export async function createPathPlannerDist(root: string): P), code:ts (const pathplannerDistDir = await createPathPlannerDist(root)), code:bash (bun run check:fix), code:ts (test("templates PathPlanner routes with bounded cardinality"), code:ts (if (path.startsWith("/pathplanner/")) return "/pathplanner/*), code:ts (if (suffix.startsWith("/api/deploy-files/"))) (+35 more) ### Community 13 - "Control Plane Route Test Suite" -Cohesion: 0.06 -Nodes (34): activeWorkspaces, DockerStatsPoller, DockerStatsPollerOptions, log, containerCpuPercent, containerMemoryPercent, containerStartBuckets, containerStartDuration (+26 more) +Cohesion: 0.05 +Nodes (47): Web Contracts Re-export, dpadToPov (gamepad-mapping), gamepadFrameToWpilib, WPILib XboxController Axis/Button Layout, dpadToPov (keyboard-mapping), gamepadStateToVisualizerFrame, Keyboard-as-Virtual-Gamepad Emulation, KEYBOARD_BINDINGS (+39 more) ### Community 14 - "NT4 Auto Chooser Protocol" -Cohesion: 0.11 -Nodes (23): body, body, body, body, absolute, refs, headers, received (+15 more) +Cohesion: 0.07 +Nodes (24): chooserRoots(), decodeMsgPack(), displayKey(), encodeMsgPack(), isChooser(), normalizeTopicName(), Nt4AutoChooserBridge, Nt4AutoChooserBridgeOptions (+16 more) ### Community 15 - "E2E Runtime & Resilience Specs" -Cohesion: 0.11 -Nodes (31): parseDockerStatsLine(), parsePercent(), defaultDockerRunner(), dockerError(), inspectContainer(), inspectContainerOrThrow(), inspectContainers(), runDocker() (+23 more) +Cohesion: 0.05 +Nodes (45): listWorkspaceDiskLimitDevices, parseDockerStatsLine, runtimeFromLease, statusFromLease, upstreamEndpoints, dockerPortBindError, inspectContainer, inspectContainers (+37 more) ### Community 16 - "Container Runtime Status Helpers" -Cohesion: 0.05 -Nodes (37): 10. Become the first admin, 1. Create and configure the GCP project, 2. Create the Terraform state bucket, 3. Configure Terraform variables, 4. Apply Terraform, 5. Populate Secret Manager, 6. Add a DNS A record, 7. Reset the VM to render config (+29 more) +Cohesion: 0.06 +Nodes (38): join, ExtensionRecord, fixture(), manifest(), old, reconcileScript, record(), repoRoot (+30 more) ### Community 17 - "Driver Station Auto/Console Panels" -Cohesion: 0.09 -Nodes (31): addAllowlistEntry(), AllowlistData, EMPTY, getAllowlist(), isEmailAllowed(), loadAllowlist(), normalize(), normalizeEntry() (+23 more) +Cohesion: 0.06 +Nodes (25): sendUpstreamWebSocketMessage(), AppSocket, ControlApp, ControlAppOptions, GamepadSocketData, HalSimSocketData, ImportSocketData, LessonLoadSocketData (+17 more) ### Community 18 - "Auth & Allowlist Test Bodies" -Cohesion: 0.09 -Nodes (10): setAllowlistPath(), AppStorage, ensureWorkspaceFiles(), log, nowIso(), projectPathFor(), randomId(), RunJobRow (+2 more) +Cohesion: 0.06 +Nodes (40): slugFromEmail, GamepadSessions (gamepad.ts), Gamepad release/close safety-disables joystick and driver station, GamepadSessions test suite, HalSimBridge (halsim.ts), HalSimBridgeUnavailableError (halsim.ts), HalSimBridge test suite, createFakeDocker (+32 more) ### Community 19 - "WebSocket Upstream Proxy" -Cohesion: 0.07 -Nodes (35): AutoPanel, preferredChooser, CodeStatusPill, codeStatusFromRun, ConsolePanel, ConsoleViewport, ControlsPanel, GamepadVisualizer (+27 more) +Cohesion: 0.05 +Nodes (37): 10. Become the first admin, 1. Create and configure the GCP project, 2. Create the Terraform state bucket, 3. Configure Terraform variables, 4. Apply Terraform, 5. Populate Secret Manager, 6. Add a DNS A record, 7. Reset the VM to render config (+29 more) ### Community 20 - "Scope Pane & Demo Banner" -Cohesion: 0.11 -Nodes (29): AssetManifest, contentTypeFor(), handleUploadAsset(), HAS_PROC_SELF_FD, pathplannerResponse(), readScopeAssetManifest(), safeRelativeAssetPath(), scopeResponse() (+21 more) +Cohesion: 0.14 +Nodes (33): parseDeployFilePath(), halsimWebSocketResponse(), HOP_BY_HOP_HEADERS, log, nt4AliveResponse(), nt4WebSocketResponse(), probeVscodeReady(), requestedProtocols() (+25 more) ### Community 21 - "Import URL Security Tests" -Cohesion: 0.06 -Nodes (30): AuditLogEntry, adminCookie, body, fakeDocker, rows, student, studentCookie, workspace (+22 more) +Cohesion: 0.11 +Nodes (21): body, body, body, body, absolute, refs, test, cookieHeader() (+13 more) ### Community 22 - "Sim Pane Switcher & Topbar" -Cohesion: 0.1 -Nodes (6): runtimeFromLease(), upstreamEndpoints(), dockerPortBindError(), LocalDockerRuntimeProvider, v2LabelsMatch(), release +Cohesion: 0.07 +Nodes (35): AutoPanel, preferredChooser, CodeStatusPill, codeStatusFromRun, ConsolePanel, ConsoleViewport, ControlsPanel, GamepadVisualizer (+27 more) ### Community 23 - "Code Status Pill Component" -Cohesion: 0.08 -Nodes (27): BigButton(), BigButtonProps, EnableDisableRow(), EnableDisableRowProps, onSetEnabled, TONE_ACTIVE, MODE_CLASSES, MODE_LABELS (+19 more) +Cohesion: 0.1 +Nodes (16): BridgeEntry, DEFAULT_DRIVER_STATION, defaultSnapshot(), DriverStationState, HalSimBridge, HalSimBridgeOptions, HalSimBridgeSnapshot, HalSimMessage (+8 more) ### Community 24 - "Container Lifecycle Test Suite" Cohesion: 0.12 -Nodes (31): parseDeployFilePath(), halsimWebSocketResponse(), HOP_BY_HOP_HEADERS, log, nt4AliveResponse(), nt4WebSocketResponse(), probeVscodeReady(), requestedProtocols() (+23 more) +Nodes (27): defaultDockerRunner(), dockerError(), dockerPortBindError(), inspectContainer(), inspectContainerOrThrow(), inspectContainers(), runDocker(), runDockerCli() (+19 more) ### Community 25 - "Admin Backup/Restore Test Suite" Cohesion: 0.08 Nodes (22): PreviewProjectOptions, RED_PIXEL_PNG, reportClassHtml(), reportCss(), reportIndexHtml(), reportJs(), seedPreviewProject(), write() (+14 more) ### Community 26 - "Better Auth Providers & Storage" -Cohesion: 0.08 -Nodes (21): PreviewFrame, PreviewPane(), PreviewPaneProps, PreviewPlaceholderProps, Doc, fetchMock, firstKey, GUIDE (+13 more) +Cohesion: 0.12 +Nodes (3): runtimeFromLease(), LocalDockerRuntimeProvider, release ### Community 27 - "AdvantageScope Patch Application" -Cohesion: 0.12 -Nodes (26): AdminRouteContext, handleAdminRoute(), log, createProjectArchive(), directorySizeBytes(), restoreProjectArchive(), runTar(), apiErrorResponse() (+18 more) +Cohesion: 0.07 +Nodes (26): audit, body, classroom, cleared, cookie, ended, expired, [guest] (+18 more) ### Community 28 - "Allowlist Management E2E" -Cohesion: 0.06 -Nodes (28): bashCalls, cloneCall, ctx, importer, mock, row, workspace, GithubImportContext (+20 more) +Cohesion: 0.1 +Nodes (18): classroom, guests, leave, switchProject, headers, received, seedRuntimeRunning(), seedWorkspaceProject() (+10 more) ### Community 29 - "Admin Backup Archive Routes" -Cohesion: 0.1 -Nodes (27): ControlConfigInput, defaultContainerUser(), defaultDataDir, defaultHalsimPortRange, defaultSimPortRange, defaultVscodePortRange, DISABLED_DISK_LIMIT_VALUES, envContainerUser() (+19 more) +Cohesion: 0.07 +Nodes (28): Adding an entry, Admin API break-glass, Audit log, Between sessions, Checking and adjusting the cap at runtime, code:bash (docker compose exec control coderunner ), code:bash (# Remove all entries before a date), code:bash (# Overall system status: workspaces, container states, activ) (+20 more) ### Community 30 - "Lesson Catalog Authoring & Gradle Cache" -Cohesion: 0.09 -Nodes (19): AutoPanel(), AutoPanelProps, ControlsPanel(), DriverStation(), DriverStationProps, change, { container, rerender }, props (+11 more) +Cohesion: 0.1 +Nodes (21): SimPanePanels(), SimPanePanelsProps, SimPaneTab, SimPaneTabs(), SimPaneTabSelector(), SimPaneTabsProps, onPreviewActivated, pathplannerTab (+13 more) ### Community 31 - "Sim API & Chooser Announcements" Cohesion: 0.07 -Nodes (26): audit, body, classroom, cleared, cookie, ended, expired, [guest] (+18 more) +Nodes (21): audit, before, { classroom }, { classrooms }, ClassroomView, deletes, end, expired (+13 more) ### Community 32 - "Deploy Files & WS Origin Guards" Cohesion: 0.07 -Nodes (28): Adding an entry, Admin API break-glass, Audit log, Between sessions, Checking and adjusting the cap at runtime, code:bash (docker compose exec control coderunner ), code:bash (# Remove all entries before a date), code:bash (# Overall system status: workspaces, container states, activ) (+20 more) +Nodes (26): code:bash (gcloud compute snapshots create coderunner-data-eoy2026 \), code:bash (gcloud compute snapshots delete coderunner-data-eoy2026), code:bash (gcloud compute snapshots describe coderunner-data-eoy2026 \), code:bash (gcloud compute instances delete coderunner --zone=northameri), code:bash (gcloud compute disks delete coderunner-data --zone=northamer), code:bash (# Only if you choose to release the IP), code:bash (terraform state rm google_compute_instance.coderunner google), code:hcl (resource "google_compute_disk" "data" {) (+18 more) ### Community 33 - "Grafana Alloy & Cloudflare Deploy Config" -Cohesion: 0.1 -Nodes (17): classroom, guests, leave, switchProject, AppFixtures, seedRuntimeRunning(), seedWorkspaceProject(), cookie (+9 more) +Cohesion: 0.07 +Nodes (26): Build times out, code:bash (# Confirm Docker is running), code:bash (SIM_PORT_RANGE=25810-25999), code:bash (# 1. Prune run logs (safest, often largest single contributo), code:bash (bun run backup), code:bash (docker compose restart control), code:bash (curl -H "Authorization: Bearer $ADMIN_TOKEN" \), code:bash (stat -c '%g' /var/run/docker.sock # e.g. 999) (+18 more) ### Community 34 - "Control Plane Config Loading" -Cohesion: 0.1 -Nodes (21): SimPanePanels(), SimPanePanelsProps, SimPaneTab, SimPaneTabs(), SimPaneTabSelector(), SimPaneTabsProps, onPreviewActivated, pathplannerTab (+13 more) +Cohesion: 0.08 +Nodes (24): aliceWorkspace, bobWorkspace, byName, calls, config, cookie, expectedName, fakeDocker (+16 more) ### Community 35 - "Public Health/OpenAPI E2E Specs" -Cohesion: 0.09 -Nodes (23): join, ExtensionRecord, fixture(), manifest(), old, reconcileScript, record(), repoRoot (+15 more) +Cohesion: 0.08 +Nodes (25): addBody, adminCookie, after, aliceWorkspace, backedUpProject, backupsDir, bob, bobWorkspace (+17 more) ### Community 36 - "Structured Logging" -Cohesion: 0.07 -Nodes (22): audit, before, { classroom }, { classrooms }, ClassroomView, deletes, end, expired (+14 more) +Cohesion: 0.16 +Nodes (5): consumeLines(), lineLooksReady(), randomRunId(), runLogPath(), RunManager ### Community 37 - "Admin App Allowlist Page" Cohesion: 0.11 -Nodes (17): CATALOG, fetchMock, { result }, useLessons(), UseLessonsReturn, INITIAL_STATE, ProjectSwapKind, ProjectSwapState (+9 more) +Nodes (14): BundledCatalogSource, CatalogManifest, CatalogSource, findModuleOrThrow(), log, parseCatalogRepo(), RemoteCatalogSource, sortByOrder() (+6 more) ### Community 38 - "Allowlist Core Module" -Cohesion: 0.12 -Nodes (19): LayoutMenu(), Avatar(), formatEndsAt(), leaveClassroom(), signOut(), UserMenu(), UserMenuProps, DropdownMenu() (+11 more) +Cohesion: 0.09 +Nodes (7): AppFixtures, WorkspacePage, status, wsp, Deps, dialog, robotCard ### Community 39 - "Self-Inspection (Container Auto-Detect)" -Cohesion: 0.07 -Nodes (26): code:bash (gcloud compute snapshots create coderunner-data-eoy2026 \), code:bash (gcloud compute snapshots delete coderunner-data-eoy2026), code:bash (gcloud compute snapshots describe coderunner-data-eoy2026 \), code:bash (gcloud compute instances delete coderunner --zone=northameri), code:bash (gcloud compute disks delete coderunner-data --zone=northamer), code:bash (# Only if you choose to release the IP), code:bash (terraform state rm google_compute_instance.coderunner google), code:hcl (resource "google_compute_disk" "data" {) (+18 more) +Cohesion: 0.11 +Nodes (16): IDELayout(), IDELayoutProps, FakeResizeObserver, useSplit(), PaneVisibility, readVisibility(), restored, { result } (+8 more) ### Community 40 - "CI/Test Command Reference" -Cohesion: 0.07 -Nodes (26): Build times out, code:bash (# Confirm Docker is running), code:bash (SIM_PORT_RANGE=25810-25999), code:bash (# 1. Prune run logs (safest, often largest single contributo), code:bash (bun run backup), code:bash (docker compose restart control), code:bash (curl -H "Authorization: Bearer $ADMIN_TOKEN" \), code:bash (stat -c '%g' /var/run/docker.sock # e.g. 999) (+18 more) +Cohesion: 0.15 +Nodes (22): isOpenFileInsideRoot(), ASSET_CONTENT_TYPES, discoverDocuments(), documentCsp(), documentKindFor(), errorDocument(), log, documentShell() (+14 more) ### Community 41 - "Audit Log & Runtime Config Schema" -Cohesion: 0.11 -Nodes (15): DemoBanner(), EditorPane(), EditorPaneProps, ScopePane, EditorReachability, EditorStatus, { result }, useEditorReachability() (+7 more) +Cohesion: 0.08 +Nodes (21): RateLimitError, bobCookie, body, clearedCache, cloneCall, cookie, copyCall, ctx (+13 more) ### Community 42 - "E2E Workspace Fixtures" -Cohesion: 0.16 -Nodes (5): consumeLines(), lineLooksReady(), randomRunId(), runLogPath(), RunManager +Cohesion: 0.14 +Nodes (23): applyAdvantageScopePatches(), ascopeRoot, CommandResult, patchDir, patchFiles(), repoRoot, run(), ascopeLiteStatic (+15 more) ### Community 43 - "Gamepad Input Mapping" Cohesion: 0.08 -Nodes (24): aliceWorkspace, bobWorkspace, byName, calls, config, cookie, expectedName, fakeDocker (+16 more) +Nodes (22): aliceConnection, aliceMessages, aliceRun, aliceRunId, aliceWorkspace, baseOptions, bobConnection, bobMessages (+14 more) ### Community 44 - "PathPlanner Integration Deploy Pipeline" -Cohesion: 0.08 -Nodes (25): addBody, adminCookie, after, aliceWorkspace, backedUpProject, backupsDir, bob, bobWorkspace (+17 more) +Cohesion: 0.11 +Nodes (15): RunCommandFactory, announceChooser(), body, controlled, cookie, encodeMsgPack(), fakeDocker, FakeWebSocket (+7 more) ### Community 45 - "Gamepad Lease Management" -Cohesion: 0.14 -Nodes (23): applyAdvantageScopePatches(), ascopeRoot, CommandResult, patchDir, patchFiles(), repoRoot, run(), ascopeLiteStatic (+15 more) +Cohesion: 0.08 +Nodes (23): 1. Choose the new version, 1. Clone and configure, 2. Pull the new images and restart the control plane, 2. Start and verify, 3. Rebuild student workspaces, 3. Sign in and allow students, 4. Verify the update, code:bash (git clone https://github.com/mathewdunne/CodeRunner.git Code) (+15 more) ### Community 46 - "E2E Workspace Fixtures" Cohesion: 0.1 -Nodes (25): Grafana Alloy config template (config.alloy.tmpl), Alloy log pipeline bounded active-series design, BACKEND_ORIGIN Pages secret, Cloudflare Offline Page, Pages Function catch-all [[path]].ts, deploy/cloudflare/wrangler.toml, coderunner-ops.json dashboard, Deployment Overview (+17 more) +Nodes (24): ADMIN_TOKEN admin API break-glass, data/allowlist.json, data/app.db (SQLite), Audit log system, bun run audit:prune, Backups doc, Capacity and Sizing doc, CODE_DISK_READ_LIMIT env var (+16 more) ### Community 47 - "Run Lifecycle Test Fixtures" -Cohesion: 0.11 -Nodes (25): Authoring Lesson Modules doc, Bundled lesson catalog (catalog/), catalog/modules/robot-starter license files, github.com/mathewdunne/coderunner-lessons, Decision 010: Gradle Project Cache Isolation for Sim and LSP, --project-cache-dir $HOME/.gradle-project-sim, stop-sim.sh, V1-10 three-user smoke test (+17 more) +Cohesion: 0.12 +Nodes (19): Auth, AuthCallbacks, createAuth(), log, slugFromEmail(), slugify(), classroomPlugin(), buildAuth0Plugin() (+11 more) ### Community 48 - "Container Lease Cleanup CLI" -Cohesion: 0.11 -Nodes (15): RunCommandFactory, announceChooser(), body, controlled, cookie, encodeMsgPack(), fakeDocker, FakeWebSocket (+7 more) +Cohesion: 0.13 +Nodes (22): ANSI, colorize(), configureLogging(), createJsonSink(), createSink(), formatAttrs(), formatAttrValue(), formatRecord() (+14 more) ### Community 49 - "Backup Database CLI" -Cohesion: 0.08 -Nodes (22): aliceConnection, aliceMessages, aliceRun, aliceRunId, aliceWorkspace, baseOptions, bobConnection, bobMessages (+14 more) +Cohesion: 0.11 +Nodes (23): AdminApp, AdminLayout, Tab (type), Allowlist (admin page), fetchAllowlist, App (default export), FallbackRedirect, RootIndex (+15 more) ### Community 50 - "PathPlanner Deploy File Schemas" -Cohesion: 0.16 -Nodes (21): ASSET_CONTENT_TYPES, discoverDocuments(), documentCsp(), documentKindFor(), errorDocument(), log, documentShell(), escapeHtml() (+13 more) +Cohesion: 0.11 +Nodes (23): BACKEND_ORIGIN Pages secret, Cloudflare Offline Page, Pages Function catch-all [[path]].ts, deploy/cloudflare/wrangler.toml, coderunner-ops.json dashboard, Deployment Overview, CodeRunner Docs Overview, c4-standard-4 GCE VM (+15 more) ### Community 51 - "Gamepad/Driver Station Zod Schemas" -Cohesion: 0.08 -Nodes (23): 1. Choose the new version, 1. Clone and configure, 2. Pull the new images and restart the control plane, 2. Start and verify, 3. Rebuild student workspaces, 3. Sign in and allow students, 4. Verify the update, code:bash (git clone https://github.com/mathewdunne/CodeRunner.git Code) (+15 more) +Cohesion: 0.13 +Nodes (14): statusFromLease(), removeCodeContainer(), removeCodeVolume(), stopWorkspaceSim(), codeContainerName(), codeVolumeName(), workspaceHomePath(), toHostPath() (+6 more) ### Community 52 - "ControlApp Test Harness Socket Types" -Cohesion: 0.11 -Nodes (24): collectFiles, deployFilesSnapshotResponse, parseDeployFilePath, isAllowedWebSocketOrigin, requireWebSocketOrigin, halsimWebSocketResponse, nt4AliveResponse, nt4WebSocketResponse (+16 more) +Cohesion: 0.09 +Nodes (21): `bun run e2e`: Playwright mocked tier, `bun run e2e:security`: security E2E tests, `bun run e2e:workspace-java`: real Java workspace smoke, `bun run test`: control-plane unit and integration tests, `bun run test:web`: frontend unit and component tests, `bun run verify`: full CI gate, code:bash (bunx playwright install chromium), code:bash (bun run test) (+13 more) ### Community 53 - "Docker Client & Lifecycle" -Cohesion: 0.1 -Nodes (24): ADMIN_TOKEN admin API break-glass, data/allowlist.json, data/app.db (SQLite), Audit log system, bun run audit:prune, Backups doc, Capacity and Sizing doc, CODE_DISK_READ_LIMIT env var (+16 more) +Cohesion: 0.11 +Nodes (22): Admin allowlist endpoints, addAllowlistEntry (auth/allowlist), isEmailAllowed (auth/allowlist), loadAllowlist (auth/allowlist), reloadAllowlist, removeAllowlistEntry, saveAllowlist, createApp (app.ts) (+14 more) ### Community 54 - "Architecture & Access Control Docs" -Cohesion: 0.11 -Nodes (23): AdminApp, AdminLayout, Tab (type), Allowlist (admin page), fetchAllowlist, App (default export), FallbackRedirect, RootIndex (+15 more) +Cohesion: 0.1 +Nodes (18): aliceCookie, bobCookie, byPath, cookie, docker, encoded, listed, names (+10 more) ### Community 55 - "Auto Panel & Gamepad Channel" -Cohesion: 0.09 -Nodes (21): `bun run e2e`: Playwright mocked tier, `bun run e2e:security`: security E2E tests, `bun run e2e:workspace-java`: real Java workspace smoke, `bun run test`: control-plane unit and integration tests, `bun run test:web`: frontend unit and component tests, `bun run verify`: full CI gate, code:bash (bunx playwright install chromium), code:bash (bun run test) (+13 more) +Cohesion: 0.16 +Nodes (19): defaultContainerUser(), defaultDataDir, defaultHalsimPortRange, defaultSimPortRange, defaultVscodePortRange, DISABLED_DISK_LIMIT_VALUES, envContainerUser(), loadControlConfig() (+11 more) ### Community 56 - "HALSim Bridge State Application" -Cohesion: 0.12 -Nodes (22): handleAdminRoute, applySecurityHeaders, dispatch, fetch (request handler), directorySizeBytes, handleUploadAsset, pathplannerResponse, readScopeAssetManifest (+14 more) +Cohesion: 0.14 +Nodes (17): DEFAULT_NETWORKS, deriveComposeProject(), deriveContainerUser(), deriveHostDataDir(), deriveNetwork(), inspectFailureMessage(), notDerived(), readComposeProject() (+9 more) ### Community 57 - "HALSim/NT4 WebSocket Responses" -Cohesion: 0.14 -Nodes (18): DEFAULT_NETWORKS, deriveComposeProject(), deriveContainerUser(), deriveHostDataDir(), deriveNetwork(), inspectFailureMessage(), notDerived(), readComposeProject() (+10 more) +Cohesion: 0.1 +Nodes (19): Admin surface, Affected code (expected), Agreed requirements, Approach, Classroom Join Codes — Design, code:sql (CREATE UNIQUE INDEX IF NOT EXISTS idx_user_classroom_guest), Data model, Docs (+11 more) ### Community 58 - "Asset Upload & Manifest" -Cohesion: 0.13 -Nodes (12): AppOptions, defaultRunCommandFactory, startFakeHalsim(), startFakeVscode(), AppFixture, FakeHalsimHandle, FakeVscodeHandle, SeededUser (+4 more) +Cohesion: 0.1 +Nodes (20): 002 — AdvantageScope Lite hosted standalone, 1. `spawnSync(npmCmd, [...], { shell: false })` returns exit `null` on Windows, 2. AS Lite ships a `lite/static/` directory inside the submodule with `index.html` and `popups.css`, 3. Git symlinks under `lite/static/` checked out as 9-byte text files on Windows, 4. AS Lite expects `GET /assets` and `GET /assets//` server routes, 5. AS submodule's `postinstall` is heavy, 6. AS upstream prints `npm audit` warnings about transitive vulns, AdvantageScope as a git submodule pinned to a release tag (+12 more) ### Community 59 - "Deploy Files Security Test Suite" -Cohesion: 0.14 -Nodes (14): GamepadFrame, GamepadInfo, listConnectedGamepads(), makeLabel(), FakePad, { result }, useGamepad(), UseGamepadResult (+6 more) +Cohesion: 0.11 +Nodes (21): Admin role + break-glass token, Architecture doc, Audit log, Cloudflare Pages Function ([[path]].ts), CODERUNNER_ADMIN_EMAIL bootstrap, Control plane container privileges (non-root, socket access), Decision 031: Containerized Control Plane (referenced), Demo mode (+13 more) ### Community 60 - "Run Manager Command Factory" -Cohesion: 0.1 -Nodes (18): aliceCookie, bobCookie, byPath, cookie, docker, encoded, listed, names (+10 more) +Cohesion: 0.12 +Nodes (21): Classroom-density memory defaults, coderunner-workspace bind-mount contract, coderunner-workspace conservative JVM/Gradle memory bounds, V2 code container (coderunner-workspace) README, Decision 015 (HALSim control protocol), Decision 016 (imported-project sim compat), Decision 024: Container Memory Budget, Decision 025 (two-phase sim runner) (+13 more) ### Community 61 - "Import Pipeline Test Fixtures" -Cohesion: 0.1 -Nodes (20): afterFirst, afterSecond, aliceCookie, allowlistPath, baseOptions, boss, coach, cookie (+12 more) +Cohesion: 0.17 +Nodes (10): addAllowlistEntry(), allowlist.spec.ts (auth), e2e/fixtures/app.ts fixture (ControlApp harness), Commits 066141e / 1a4f5e6 (Vite base path revert), loginAs() / cookieHeader() auth fixture, oauth-callback.spec.ts (OAuth callback flow), parseGitHubUrl() (SSRF-safe import URL parsing), requireWorkspaceOwnership() (default-deny routing gate) (+2 more) ### Community 62 - "Path Planner Pane.test" -Cohesion: 0.1 -Nodes (19): Admin surface, Affected code (expected), Agreed requirements, Approach, Classroom Join Codes — Design, code:sql (CREATE UNIQUE INDEX IF NOT EXISTS idx_user_classroom_guest), Data model, Docs (+11 more) +Cohesion: 0.11 +Nodes (21): AdminPage (page object), CLI Reference Doc, Decision 036: VSCodium Web Migration, Decision 037: Gradle Wrapper Alias and Extension Pins, Decision 038: Java Tooling Compatibility and Smoke, bun run e2e (Playwright Mocked Tier), bun run e2e:security (Security E2E Tests), bun run e2e:workspace-java (Real Java Workspace Smoke) (+13 more) ### Community 63 - "Container Capacity & V2 Acceptance Decisions" -Cohesion: 0.1 -Nodes (20): 002 — AdvantageScope Lite hosted standalone, 1. `spawnSync(npmCmd, [...], { shell: false })` returns exit `null` on Windows, 2. AS Lite ships a `lite/static/` directory inside the submodule with `index.html` and `popups.css`, 3. Git symlinks under `lite/static/` checked out as 9-byte text files on Windows, 4. AS Lite expects `GET /assets` and `GET /assets//` server routes, 5. AS submodule's `postinstall` is heavy, 6. AS upstream prints `npm audit` warnings about transitive vulns, AdvantageScope as a git submodule pinned to a release tag (+12 more) +Cohesion: 0.15 +Nodes (15): clearContainerLeases(), dbPathFromEnv(), DockerCommandResult, DockerRunner, main(), parseContainerNames(), rebuildWorkspaces(), RebuildWorkspacesOptions (+7 more) ### Community 64 - "Contracts Property-Based Tests" Cohesion: 0.1 -Nodes (20): AdvantageScope Lite NT4 client, e2e/fixtures/app.ts (ControlApp test fixture), E2E_TEST=1 gates Better Auth testUtils plugin, e2e/fixtures/fake-halsim.ts, e2e/fixtures/fake-nt4.ts, e2e/fixtures/fake-vscode.ts, e2e/global-setup.ts, MockWorkspaceRuntimeProvider (+12 more) +Nodes (19): 1. Add the origin A record, 2. Verify the Caddyfile has the origin vhost, 3. Bootstrap the Cloudflare Pages project, 4. Set `BACKEND_ORIGIN` as a Pages secret, 5. Add the custom domain in Cloudflare, 6. Add GitHub Actions variables, Cloudflare Offline Page, code:block1 (student browser ──443──> Cloudflare Pages (coderunner)) (+11 more) ### Community 65 - "Admin Allowlist Endpoints" -Cohesion: 0.11 -Nodes (16): DEFAULT_PORT, GamepadLease, GamepadLeaseResolver, GamepadMessageOutcome, GamepadStatus, log, SessionState, HalSimBridgeUnavailableError (+8 more) +Cohesion: 0.1 +Nodes (19): 001 — Sim container architecture, 1. Missing `WPILibNewCommands.json` vendor dep, 2. Dockerfile `|| true` swallowed gradle build failure, 3. Image size came in at 2.25 GB, not the 1.0–1.4 GB plan estimate, 4. Wrapper-zip warning is cosmetic, Context, Decisions, `eclipse-temurin:17-jdk-jammy` base image (+11 more) ### Community 66 - "AdvantageScope Lite & Docusaurus" Cohesion: 0.15 -Nodes (15): clearContainerLeases(), dbPathFromEnv(), DockerCommandResult, DockerRunner, main(), parseContainerNames(), rebuildWorkspaces(), RebuildWorkspacesOptions (+7 more) +Nodes (12): AppOptions, defaultRunCommandFactory, startFakeHalsim(), startFakeVscode(), AppFixture, FakeHalsimHandle, FakeVscodeHandle, SeededUser (+4 more) ### Community 67 - "Workspace Container Bind-Mount & Memory Bounds" -Cohesion: 0.1 -Nodes (19): 1. Add the origin A record, 2. Verify the Caddyfile has the origin vhost, 3. Bootstrap the Cloudflare Pages project, 4. Set `BACKEND_ORIGIN` as a Pages secret, 5. Add the custom domain in Cloudflare, 6. Add GitHub Actions variables, Cloudflare Offline Page, code:block1 (student browser ──443──> Cloudflare Pages (coderunner)) (+11 more) +Cohesion: 0.15 +Nodes (11): PreviewFrame, PreviewPane(), PreviewPaneProps, PreviewPlaceholderProps, findDefaultDocument(), PreviewDocumentsState, previewFileUrl(), fetchMock (+3 more) ### Community 68 - "Slug/Login Property Tests" -Cohesion: 0.1 -Nodes (19): 001 — Sim container architecture, 1. Missing `WPILibNewCommands.json` vendor dep, 2. Dockerfile `|| true` swallowed gradle build failure, 3. Image size came in at 2.25 GB, not the 1.0–1.4 GB plan estimate, 4. Wrapper-zip warning is cosmetic, Context, Decisions, `eclipse-temurin:17-jdk-jammy` base image (+11 more) +Cohesion: 0.12 +Nodes (11): HalSimBridgeUnavailableError, { bridge }, { bridge, socket }, disable, FakeWebSocket, joystick, messages, outcome (+3 more) ### Community 69 - "Scripts" -Cohesion: 0.12 -Nodes (20): workspaces.current_module column, Admin allowlist endpoints, addAllowlistEntry (auth/allowlist), isEmailAllowed (auth/allowlist), loadAllowlist (auth/allowlist), reloadAllowlist, removeAllowlistEntry, saveAllowlist (+12 more) +Cohesion: 0.13 +Nodes (15): first, NUM_RUNS, second, cases, expectRejected(), result, CatalogLoadContext, ImportContext (+7 more) ### Community 70 - "Mock Workspace Runtime Provider" -Cohesion: 0.16 -Nodes (16): digitalAxis(), dpadToPov(), gamepadStateToVisualizerFrame(), isMappedKeyboardCode(), KEYBOARD_BINDINGS, KeyboardBindingGroup, keyboardCodesToWpilib(), MAPPED_CODES (+8 more) - -### Community 71 - "WebSocket Router Test Suite" Cohesion: 0.11 Nodes (18): Authentication, Capturing a session, code:block1 (14:23:01.482 INFO [control.runs] run started workspaceId=a), code:json ({"timestamp":"2026-05-21T14:23:01.482Z","level":"info","cate), code:bash (docker compose logs -f control | tee coderunner-$(date +%Y%m), code:bash (# Manual probe), code:bash (curl http://localhost:4000/healthz), code:json ({"ok":true,"service":"control","version":"v2-3"}) (+10 more) -### Community 72 - "Deploy Files Path Safety" +### Community 71 - "WebSocket Router Test Suite" Cohesion: 0.11 Nodes (18): Capacity and Sizing, Checking disk usage, Cleaning up, code:bash (# Tighter cap for a memory-constrained host (expect slower c), code:block2 (available_for_containers = total_RAM - 4 GB), code:bash (# Per-container CPU and memory (one-shot)), code:bash (# Overall free space), code:bash (bun run docker:cleanup) (+10 more) +### Community 72 - "Deploy Files Path Safety" +Cohesion: 0.15 +Nodes (19): Authoring Lesson Modules doc, Bundled lesson catalog (catalog/), catalog/modules/robot-starter license files, github.com/mathewdunne/coderunner-lessons, Google API Services User Data Policy, hello-world module, LESSONS_CATALOG_REPO env var, Lessons-are-gitless design principle (+11 more) + ### Community 73 - "Java Tooling & Test Tiers" -Cohesion: 0.13 -Nodes (19): Admin role + break-glass token, Architecture doc, Audit log, Cloudflare Pages Function ([[path]].ts), CODERUNNER_ADMIN_EMAIL bootstrap, Control plane container privileges (non-root, socket access), Decision 031: Containerized Control Plane (referenced), Demo mode (+11 more) +Cohesion: 0.11 +Nodes (17): bobCookie, body, cookie, docker, evilDir, evilPath, example, newFile (+9 more) ### Community 74 - "VSCodium Editor Container Defaults" -Cohesion: 0.21 -Nodes (3): defaultSnapshot(), HalSimBridge, upstreamUrlFor() +Cohesion: 0.11 +Nodes (17): Authoring Lesson Modules, code:text (modules.json ← the catalog manifest (required, ), code:json ({), code:json ({), Example curriculum, Module fields, `plain-java`, Publishing (+9 more) ### Community 75 - "Contracts Schema Unit Tests" Cohesion: 0.11 -Nodes (17): bobCookie, body, cookie, docker, evilDir, evilPath, example, newFile (+9 more) +Nodes (17): Auto-import on Tab (additionalTextEdits), code:dockerfile (FROM gitpod/openvscode-server:1.105.1), code:bash (cd /tmp/frc-spike-openvscode), Container boots and serves (:3000), Ctrl-click into library source (jdt:// URI), Decision 011: V2 Editor Spike — openvscode-server with redhat.java and WPILib, Decisions, Docker Hub vs GitHub Releases (+9 more) ### Community 76 - "Import Manager (Git Clone Staging)" Cohesion: 0.11 -Nodes (17): Authoring Lesson Modules, code:text (modules.json ← the catalog manifest (required, ), code:json ({), code:json ({), Example curriculum, Module fields, `plain-java`, Publishing (+9 more) +Nodes (17): 003 — Minimal web shell, 1. Headless preview screenshot hangs when AS Lite iframe is loading, 2. Orphaned Vite child after `TaskStop` on the npm wrapper, 3. `WARNING:StorageManager: settings timeout, using defaults` in the console, 4. AS Lite tab-controls panel was clipped (initial layout), AS Lite via iframe to `http://localhost:8080`, not bundled, code:block1 ("editor scope"), Context (+9 more) ### Community 77 - "Demo Mode Session Seeding" Cohesion: 0.11 -Nodes (17): Auto-import on Tab (additionalTextEdits), code:dockerfile (FROM gitpod/openvscode-server:1.105.1), code:bash (cd /tmp/frc-spike-openvscode), Container boots and serves (:3000), Ctrl-click into library source (jdt:// URI), Decision 011: V2 Editor Spike — openvscode-server with redhat.java and WPILib, Decisions, Docker Hub vs GitHub Releases (+9 more) +Nodes (17): 006 - Multi-tenancy spike findings, code:text (alice alive 200 ok), code:bash (npm run spike:multi -- up), code:text (alice: websocket open=true, initialized=true, diagnostics=0), code:text (open=true, initialized=true), code:text (GET /file?user=alice -> 200), code:text (status building at 1 ms), code:text (sim container created in 0.29s) (+9 more) ### Community 78 - "Backup/Restore & Catalog Import Flows" -Cohesion: 0.11 -Nodes (17): 003 — Minimal web shell, 1. Headless preview screenshot hangs when AS Lite iframe is loading, 2. Orphaned Vite child after `TaskStop` on the npm wrapper, 3. `WARNING:StorageManager: settings timeout, using defaults` in the console, 4. AS Lite tab-controls panel was clipped (initial layout), AS Lite via iframe to `http://localhost:8080`, not bundled, code:block1 ("editor scope"), Context (+9 more) +Cohesion: 0.19 +Nodes (18): driverStationPatchSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, gamepadStateSchema, importRequestSchema, packages/contracts/src/index.ts, packages/contracts/src/index.test.ts, isWorkspaceSlug() (+10 more) ### Community 79 - "Auth Client & Docker Runner Helpers" -Cohesion: 0.11 -Nodes (17): 006 - Multi-tenancy spike findings, code:text (alice alive 200 ok), code:bash (npm run spike:multi -- up), code:text (alice: websocket open=true, initialized=true, diagnostics=0), code:text (open=true, initialized=true), code:text (GET /file?user=alice -> 200), code:text (status building at 1 ms), code:text (sim container created in 0.29s) (+9 more) +Cohesion: 0.18 +Nodes (15): docker(), DockerResult, migratedSettings, openJavaFile(), repoRoot, startWorkspace(), stopWorkspace(), terminal (+7 more) ### Community 80 - "HALSim Test Suite" -Cohesion: 0.14 -Nodes (18): createApp (app.ts), Physical disk vs virtual device filtering for --device-read-bps, listWorkspaceDiskLimitDevices (containers/block-devices), BundledCatalogSource (catalog.ts), CatalogSource interface, createCatalogSource (catalog.ts), parseCatalogRepo (catalog.ts), RemoteCatalogSource (catalog.ts) (+10 more) +Cohesion: 0.12 +Nodes (16): first, NUM_RUNS, parsed, round, slugArb, allianceStationSchema, bridgeConnectionSchema, containerStateSchema (+8 more) ### Community 81 - "Bundled Lesson Catalog Modules" -Cohesion: 0.14 -Nodes (17): audit_log table, runtime_config table, audit log test suite, recordAuditEvent (audit.ts, tested), container concurrency cap test suite, max-active-containers cap persisted via runtime_config, adoption/restart gating, CapacityExceededError, ensureCodeContainer (containers) (+9 more) +Cohesion: 0.12 +Nodes (15): AuditLogEntry, adminCookie, body, fakeDocker, rows, student, studentCookie, workspace (+7 more) ### Community 82 - "Fake NT4 Test Server" -Cohesion: 0.15 -Nodes (18): coderunner-workspace bind-mount contract, coderunner-workspace conservative JVM/Gradle memory bounds, V2 code container (coderunner-workspace) README, Decision 015 (HALSim control protocol), Decision 016 (imported-project sim compat), Decision 025 (two-phase sim runner), Decision 025: Detach Sim JVM from Gradle, Decision 037 (Gradle daemon settings vs java.import.gradle.*) (+10 more) +Cohesion: 0.21 +Nodes (14): AdminRouteContext, handleAdminRoute(), log, createProjectArchive(), directorySizeBytes(), restoreProjectArchive(), runTar(), notFound() (+6 more) ### Community 83 - "HALSim Bridge Message Handling" -Cohesion: 0.14 -Nodes (18): bun run e2e:workspace-java (real Java workspace smoke), Code container VS Code defaults test suite, containers/code/Dockerfile, ghcr.io/mathewdunne/coderunner-workspace image, codium-server (VSCodium reh-web), Decision 033: workspace disk read limit, coderunner rebuild-workspaces CLI, scripts/image.ts (+10 more) +Cohesion: 0.12 +Nodes (16): Alloy config location, code:block1 (https://prometheus-prod-XX-prod-us-central-0.grafana.net/api), code:block2 (https://logs-prod-XXX.grafana.net/loki/api/v1/push), code:bash (echo -n 'https://prometheus-prod-XX-prod-us-central-0.grafan), code:bash (gcloud compute ssh coderunner --zone=us-central1-a --tunnel-), code:bash (cd /opt/coderunner), code:logql (# All logs from one student), code:promql (up{instance="coderunner"}) (+8 more) ### Community 84 - "Database Migrations Runner" Cohesion: 0.12 -Nodes (18): Decision 036: VSCodium Web Migration, Decision 037: Gradle Wrapper Alias and Extension Pins, Decision 038: Java Tooling Compatibility and Smoke, bun run e2e (Playwright Mocked Tier), bun run e2e:security (Security E2E Tests), bun run e2e:workspace-java (Real Java Workspace Smoke), bun run test:web (Frontend Unit/Component Tests), bun run verify (Full CI Gate) (+10 more) +Nodes (16): Browser API behavior, code:text (Browser -> stateless HTTP API -> control-plane HALSim bridge), code:json ({ "type": "", "device": "", "data": {), Context, Decision, Decision 015 — HALSim WebSocket Control Protocol, DriverStation message (`type: "DriverStation"`, `device: ""`), Future hooks (+8 more) ### Community 85 - "Asset Upload Test Suite" -Cohesion: 0.18 -Nodes (15): docker(), DockerResult, migratedSettings, openJavaFile(), repoRoot, startWorkspace(), stopWorkspace(), terminal (+7 more) +Cohesion: 0.12 +Nodes (16): Code style and CI gates, code:bash (bun install), code:bash (bun run dev:control), code:bash (bun run dev:control -- --demo), code:bash (bun run dev:web), code:bash (bun run migrate # apply all pending migrations), code:bash (bun run check:fix), code:bash (bun run verify) (+8 more) ### Community 86 - "Capacity Cap Test Suite" Cohesion: 0.12 -Nodes (16): first, NUM_RUNS, parsed, round, slugArb, allianceStationSchema, bridgeConnectionSchema, containerStateSchema (+8 more) +Nodes (16): Bind mounts, Build, code:bash (bun run docker:build:workspace), code:block2 (frc-sim.managed=true), code:bash (docker run -d \), Environment variables, Example run, First-Run Behavior (+8 more) ### Community 87 - "Disk Read Limit Config" -Cohesion: 0.21 -Nodes (16): isInsideDirectory(), isOpenFileInsideRoot(), collectFiles(), deployFileDeleteResponse(), deployFilesSnapshotResponse(), deployFileWriteResponse(), DeployWorkspace, findDeepestExistingAncestor() (+8 more) +Cohesion: 0.12 +Nodes (17): demo mode test suite, GATED_PATHS list, PUBLIC_PATHS list, Default-deny auth coverage via explicit route manifest (no route table introspection), default-deny route coverage test suite (Plan §A.7.6), DEMO_SLUG (auth/demo), DEMO_USER_ID (auth/demo), PathPlanner subtree writable, choreo subtree read-only design (+9 more) ### Community 88 - "" Cohesion: 0.12 -Nodes (16): Alloy config location, code:block1 (https://prometheus-prod-XX-prod-us-central-0.grafana.net/api), code:block2 (https://logs-prod-XXX.grafana.net/loki/api/v1/push), code:bash (echo -n 'https://prometheus-prod-XX-prod-us-central-0.grafan), code:bash (gcloud compute ssh coderunner --zone=us-central1-a --tunnel-), code:bash (cd /opt/coderunner), code:logql (# All logs from one student), code:promql (up{instance="coderunner"}) (+8 more) +Nodes (16): AdvantageKit, patches/advantagescope/ source-level patches, AdvantageScope (upstream), 001-lite-nt4-endpoint-injection.patch, AS Lite in-iframe timeout banner, Docusaurus, GitHub CLI, Licenses doc (+8 more) ### Community 89 - "AdvantageScope Lite Hosting (Archived)" -Cohesion: 0.12 -Nodes (16): Browser API behavior, code:text (Browser -> stateless HTTP API -> control-plane HALSim bridge), code:json ({ "type": "", "device": "", "data": {), Context, Decision, Decision 015 — HALSim WebSocket Control Protocol, DriverStation message (`type: "DriverStation"`, `device: ""`), Future hooks (+8 more) +Cohesion: 0.15 +Nodes (13): test (Playwright base.extend AppFixtures), e2e/fixtures/app.ts (ControlApp test fixture), E2E_TEST=1 gates Better Auth testUtils plugin, e2e/fixtures/fake-halsim.ts, e2e/fixtures/fake-vscode.ts, e2e/global-setup.ts, MockWorkspaceRuntimeProvider, e2e/fixtures/runtime.ts (+5 more) ### Community 90 - "Driver Station Page Object & Runtime Seeding" -Cohesion: 0.12 -Nodes (16): Code style and CI gates, code:bash (bun install), code:bash (bun run dev:control), code:bash (bun run dev:control -- --demo), code:bash (bun run dev:web), code:bash (bun run migrate # apply all pending migrations), code:bash (bun run check:fix), code:bash (bun run verify) (+8 more) +Cohesion: 0.13 +Nodes (16): Biome (lint/format/import org), bun run e2e (Playwright mocked tier), bun run e2e:security, bun run test (control-plane unit/integration), bun run test:web (Vitest), bun run verify (CI gate), apps/control/src/config.ts migrations resolution, --demo / CODERUNNER_DEMO_MODE flag (+8 more) ### Community 91 - "Rebuild Workspaces" Cohesion: 0.12 -Nodes (16): Bind mounts, Build, code:bash (bun run docker:build:workspace), code:block2 (frc-sim.managed=true), code:bash (docker run -d \), Environment variables, Example run, First-Run Behavior (+8 more) +Nodes (8): auth0Options, config, custom, nextIdTokenClaims, sessionsBefore, url, user, workspace ### Community 92 - "Canonical Image Naming Decisions" -Cohesion: 0.13 -Nodes (16): Biome (lint/format/import org), bun run e2e (Playwright mocked tier), bun run e2e:security, bun run test (control-plane unit/integration), bun run test:web (Vitest), bun run verify (CI gate), apps/control/src/config.ts migrations resolution, --demo / CODERUNNER_DEMO_MODE flag (+8 more) +Cohesion: 0.23 +Nodes (15): isInsideDirectory(), collectFiles(), deployFileDeleteResponse(), deployFilesSnapshotResponse(), deployFileWriteResponse(), DeployWorkspace, findDeepestExistingAncestor(), HAS_PROC_SELF_FD (+7 more) ### Community 93 - "Audit Log & Break-Glass Admin" -Cohesion: 0.12 -Nodes (16): AdvantageKit, patches/advantagescope/ source-level patches, AdvantageScope (upstream), 001-lite-nt4-endpoint-injection.patch, AS Lite in-iframe timeout banner, Docusaurus, GitHub CLI, Licenses doc (+8 more) - -### Community 94 - "Container Runtime Status Helpers" -Cohesion: 0.21 -Nodes (17): driverStationPatchSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, gamepadStateSchema, importRequestSchema, packages/contracts/src/index.ts, packages/contracts/src/index.test.ts, isWorkspaceSlug() (+9 more) +Cohesion: 0.15 +Nodes (9): bridge, enabledDisable, FakeWebSocket, flush, joystick, messages, msg, sockets (+1 more) ### Community 95 - "Network Mode Config Test" Cohesion: 0.12 -Nodes (11): cookie, snapshot, cookie, firstBody, rows, runtime2, secondBody, states (+3 more) - -### Community 97 - "Rebuild Workspaces CLI Args" -Cohesion: 0.14 -Nodes (13): BridgeEntry, DEFAULT_DRIVER_STATION, DriverStationState, HalSimBridgeOptions, HalSimBridgeSnapshot, HalSimMessage, HalSimWebSocketFactory, JoystickWireState (+5 more) - -### Community 98 - "Users CLI Script" -Cohesion: 0.12 Nodes (15): Backup options, Backups, code:bash (docker compose exec control coderunner backup), code:bash (bun run backup), code:block3 (data/backups/2026-05-16-151038/), code:bash (# Write the backup to a custom location), code:bash (bun run restore -- ), code:bash (# Preview what would be restored without writing anything) (+7 more) -### Community 99 - "Extension Reconciliation Test" +### Community 96 - "Simulation State Hook" Cohesion: 0.12 Nodes (15): Admin and Metrics, Auth and OAuth, code:bash (bun run start -- --demo), Configuration Reference, Demo mode and the `--demo` flag, Docker and Containers, Docker Compose deployment, How environment is loaded (+7 more) -### Community 100 - "Sim Api.test" +### Community 97 - "Rebuild Workspaces CLI Args" Cohesion: 0.12 Nodes (15): Can CodeRunner run offline or without internet?, Can I build or start simulation from the WPILib extension?, Can I write my own lessons?, Can students accidentally break each other's work?, Can students push code to GitHub?, Do students need accounts? What if I just want to try it?, FAQ, How do students read their README or a test report? (+7 more) -### Community 101 - "AdvantageScope Verify Script" +### Community 98 - "Users CLI Script" +Cohesion: 0.16 +Nodes (16): containers/lsp/bridge/bridge.ts, Bun-native WebSocket-to-stdio Bridge, container_leases.lsp_state column split, Generic ContainerOrchestrator, apps/web/src/java-lsp.ts, Eclipse JDT LS, Decision 008: V1 LSP Container and Bun-native Bridge, apps/control/src/app.ts (+8 more) + +### Community 99 - "Extension Reconciliation Test" Cohesion: 0.13 -Nodes (16): makeScriptedRunCommandFactory, consumeLines, defaultRunCommandFactory, dockerRunScript, lineLooksReady, RunManager (app.runs), Startup marks persisted active runs as stopped, run lifecycle and log streaming test suite (+8 more) +Nodes (14): authProvidersResponseSchema, autoChooserPatchSchema, autoChoosersResponseSchema, deployFilePathSchema, deployFilesSnapshotResponseSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, importResponseSchema (+6 more) -### Community 102 - "README" -Cohesion: 0.17 -Nodes (16): bootstrap.sh first-boot provisioning script, Caddyfile (GCE deployment), Decision 023: Metrics and Observability, Decision 023: Metrics and Observability, Decision 027: Ship control-plane logs to Grafana Cloud Loki, Decision 031 (containerized control plane), GCE cloud-init user-data.yaml, deploy/ directory README (+8 more) +### Community 100 - "Sim Api.test" +Cohesion: 0.13 +Nodes (13): SocketData, waitFor(), CloseCall, fakeDocker, FakeSocket, fakeUpstream, forwarded, hello (+5 more) + +### Community 101 - "AdvantageScope Verify Script" +Cohesion: 0.25 +Nodes (14): AssetManifest, contentTypeFor(), handleUploadAsset(), HAS_PROC_SELF_FD, pathplannerResponse(), readScopeAssetManifest(), safeRelativeAssetPath(), scopeResponse() (+6 more) ### Community 103 - "Container Isolation & HALSim Decisions" Cohesion: 0.16 -Nodes (16): containers/lsp/bridge/bridge.ts, Bun-native WebSocket-to-stdio Bridge, container_leases.lsp_state column split, Generic ContainerOrchestrator, apps/web/src/java-lsp.ts, Eclipse JDT LS, Decision 008: V1 LSP Container and Bun-native Bridge, apps/control/src/app.ts (+8 more) +Nodes (12): parseDockerStatsLine(), parsePercent(), upstreamEndpoints(), managedContainerStats(), containerRuntimeState(), CodeContainerStatus, ContainerLeaseRow, log (+4 more) ### Community 104 - "Cloudflare Pages Proxy Function" -Cohesion: 0.12 -Nodes (16): disk.tf (Terraform disk config), DriverStationPage (page object), Hardware sizing guidance for team, FAQ, Why first build/run is slow, Offline capability of CodeRunner, e2e/fixtures/gamepad-shim.ts, installGamepadShim() (+8 more) +Cohesion: 0.13 +Nodes (14): Admin role, Audit log, Authentication, code:json ({), Container isolation, Control plane container privileges, Demo mode, Email allowlist (+6 more) ### Community 105 - "Project Swap Fake Socket Test" Cohesion: 0.13 -Nodes (14): authProvidersResponseSchema, autoChooserPatchSchema, autoChoosersResponseSchema, deployFilePathSchema, deployFilesSnapshotResponseSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, importResponseSchema (+6 more) +Nodes (14): 10. Third-party software, 11. Changes to these terms, 12. Governing law, 13. Contact, 1. What CodeRunner is, 2. Who may use it, 3. Acceptable use, 4. Your code (+6 more) ### Community 106 - "Proxy Header Test Suite" -Cohesion: 0.16 -Nodes (6): FakeSocket, Listener, { rerender }, { result }, { unmount }, useRunChannel() +Cohesion: 0.13 +Nodes (14): 031 — Containerized Control Plane, Addendum — non-root control container, Admin bootstrap: `CODERUNNER_ADMIN_EMAIL`, Bind-mount path translation, code:yaml (user: "${CODERUNNER_UID:-1000}:${CODERUNNER_GID:-1000}"), `coderunner` — a dispatching CLI, not raw scripts, Consequences, Context (+6 more) + +### Community 107 - "Container Lease Schema (V1/V2)" +Cohesion: 0.19 +Nodes (15): /admin/workspaces/:id/backup route (manual per-workspace backup), S9-S11 command-injection defense tests, MockWorkspaceRuntimeProvider, admin backup/restore workspace project, Bundled/remote catalog lesson load flow (gitless), GitHub team import flow (all-branches, depth-1, keeps .git), ImportManager, ImportRateLimiter (+7 more) ### Community 108 - "Core Schema Tables & Better Auth Migration" Cohesion: 0.13 -Nodes (13): SocketData, waitFor(), CloseCall, fakeDocker, FakeSocket, fakeUpstream, forwarded, hello (+5 more) +Nodes (15): authClient (better-auth), defaultDockerRunner, inspectContainerOrThrow, configureLogging, getLogger, loadProviders, LoginPage Component, OAuthButton Component (+7 more) ### Community 109 - "WebSocket Upstream Message Send" -Cohesion: 0.13 -Nodes (14): Admin role, Audit log, Authentication, code:json ({), Container isolation, Control plane container privileges, Demo mode, Email allowlist (+6 more) +Cohesion: 0.2 +Nodes (15): MAX_ACTIVE_CONTAINERS capacity limit, Container labels + reconciliation, Decision 011: V2 Editor Spike, Decision 012: V2 Code Image, Decision 013: V2 Acceptance Pass, Decision 017: linuxserver Base Migration, First-run init behavior, gitpod/openvscode-server base image (+7 more) ### Community 110 - "Deployment Hardware & FAQ Docs" -Cohesion: 0.13 -Nodes (14): 10. Third-party software, 11. Changes to these terms, 12. Governing law, 13. Contact, 1. What CodeRunner is, 2. Who may use it, 3. Acceptable use, 4. Your code (+6 more) +Cohesion: 0.15 +Nodes (15): bun run e2e:workspace-java (real Java workspace smoke), deploy-cloudflare job in deploy.yml, ghcr.io/mathewdunne/coderunner-workspace image, deploy.yml GitHub Actions workflow, GCE docker compose stack (control/caddy/alloy), PATHPLANNER_DIST_TAG pin, coderunner rebuild-workspaces CLI, release.yml GitHub Actions workflow (+7 more) ### Community 111 - "E2E ControlApp Test Fixture Setup" -Cohesion: 0.13 -Nodes (14): 031 — Containerized Control Plane, Addendum — non-root control container, Admin bootstrap: `CODERUNNER_ADMIN_EMAIL`, Bind-mount path translation, code:yaml (user: "${CODERUNNER_UID:-1000}:${CODERUNNER_GID:-1000}"), `coderunner` — a dispatching CLI, not raw scripts, Consequences, Context (+6 more) +Cohesion: 0.17 +Nodes (15): apps/control/src/app.ts, coderunner dispatching CLI, containers/control/entrypoint.sh, apps/control/src/config.ts (ControlConfig), Configuration Reference, createApp() (ControlApp factory), containers/control/Dockerfile, scripts/fetch-dist.ts (+7 more) ### Community 112 - "AS Lite NT4 Endpoint Injection Patch" -Cohesion: 0.19 -Nodes (15): /admin/workspaces/:id/backup route (manual per-workspace backup), S9-S11 command-injection defense tests, MockWorkspaceRuntimeProvider, admin backup/restore workspace project, Bundled/remote catalog lesson load flow (gitless), GitHub team import flow (all-branches, depth-1, keeps .git), ImportManager, ImportRateLimiter (+7 more) +Cohesion: 0.14 +Nodes (13): compact, console_, disable, editor, editorBody, editorControl, freshConsole, hideEditor (+5 more) ### Community 113 - "Editor Pane Reachability" -Cohesion: 0.13 -Nodes (15): authClient (better-auth), defaultDockerRunner, inspectContainerOrThrow, configureLogging, getLogger, loadProviders, LoginPage Component, OAuthButton Component (+7 more) +Cohesion: 0.18 +Nodes (5): FakeSocket, Listener, { rerender }, { result }, { unmount } ### Community 114 - "Driver Station Switch Project Flow" -Cohesion: 0.19 -Nodes (15): MAX_ACTIVE_CONTAINERS capacity limit, Container labels + reconciliation, Decision 011: V2 Editor Spike, Decision 012: V2 Code Image, Decision 013: V2 Acceptance Pass, First-run init behavior, gitpod/openvscode-server base image, Idle auto-stop (+7 more) +Cohesion: 0.14 +Nodes (10): Doc, fetchMock, firstKey, GUIDE, README, REPORT_INDEX, { rerender }, ROOT_INDEX (+2 more) ### Community 115 - "Driver Station Switch Project Flow" -Cohesion: 0.17 -Nodes (15): AdminPage (page object), CLI Reference Doc, coderunner dispatching CLI, containers/control/entrypoint.sh, Configuration Reference, containers/control/Dockerfile, scripts/fetch-dist.ts, Quick Start (Installation) (+7 more) +Cohesion: 0.25 +Nodes (10): DistDownload, downloadAndExtract(), run(), withScratch(), artifacts, main(), repoRoot, tagArgIndex (+2 more) ### Community 116 - "Admin Workspace Backup/Restore Actions" Cohesion: 0.14 -Nodes (13): compact, console_, disable, editor, editorBody, editorControl, freshConsole, hideEditor (+5 more) +Nodes (13): 1. Prove the report delivery model, 2. Add contracts and read-only delivery, 3. Add Preview UI, 4. Validate the complete workflow, 5. Document and finish, Acceptance criteria, Existing integration points, Goal and agreed UX (+5 more) ### Community 117 - "Auto Choosers & Container Status Hooks" -Cohesion: 0.2 -Nodes (7): IDELayout(), IDELayoutProps, FakeResizeObserver, useSplit(), ResizableHandle(), ResizablePanel(), ResizablePanelGroup() +Cohesion: 0.14 +Nodes (14): code:ts (/**), code:ts (export function slugify(value: string): string {), code:ts (import type { AuditEventInput } from "../audit";), code:ts (export type AuthCallbacks = {), code:ts (plugins: [), code:ts (// Classroom guests (decision 043). Better Auth adds these c), code:ts (before: async (user) => {), code:ts (// 4. Create Better Auth instance and run its migrations) (+6 more) ### Community 118 - "Java Tooling Compatibility Decisions" Cohesion: 0.14 -Nodes (12): attacker, classroom, coachRow, firstBody, firstCookie, firstSessions, firstWorkspace, school (+4 more) +Nodes (13): Building the image locally, code:bash (bun run docker:build:workspace), code:block2 (docker build -f containers/code/Dockerfile -t ghcr.io/mathew), code:bash (bun run docker:pull:workspace), code:bash (bun run docker:build:workspace), code:bash (bun run docker:rebuild-workspaces), code:bash (bun run docker:rebuild-workspaces -- --dry-run), Editor acceptance smoke (+5 more) ### Community 119 - "Lessons & Testing Decision Docs" -Cohesion: 0.25 -Nodes (10): DistDownload, downloadAndExtract(), run(), withScratch(), artifacts, main(), repoRoot, tagArgIndex (+2 more) +Cohesion: 0.14 +Nodes (14): Physical disk vs virtual device filtering for --device-read-bps, listWorkspaceDiskLimitDevices (containers/block-devices), ControlConfig (config.ts), defaultContainerUser, envContainerUser, loadControlConfig, parseHostDataDir, codeDiskReadLimit validation against Docker byte-rate format (+6 more) ### Community 120 - "Editor Migration Decisions" -Cohesion: 0.14 -Nodes (13): 1. Prove the report delivery model, 2. Add contracts and read-only delivery, 3. Add Preview UI, 4. Validate the complete workflow, 5. Document and finish, Acceptance criteria, Existing integration points, Goal and agreed UX (+5 more) +Cohesion: 0.17 +Nodes (11): FakeNt4Handle, FakeNt4Options, startFakeNt4(), aliceMessages, aliceReady, aliceSocket, allAlice, allBob (+3 more) ### Community 121 - "CLI Reference & Control Container" -Cohesion: 0.14 -Nodes (14): code:ts (/**), code:ts (export function slugify(value: string): string {), code:ts (import type { AuditEventInput } from "../audit";), code:ts (export type AuthCallbacks = {), code:ts (plugins: [), code:ts (// Classroom guests (decision 043). Better Auth adds these c), code:ts (before: async (user) => {), code:ts (// 4. Create Better Auth instance and run its migrations) (+6 more) +Cohesion: 0.15 +Nodes (12): assetDir, assetsDir, config, cookie, createAssetZip(), formData, manifest, proc (+4 more) ### Community 122 - "Gamepad Shim E2E Test" -Cohesion: 0.14 -Nodes (13): Building the image locally, code:bash (bun run docker:build:workspace), code:block2 (docker build -f containers/code/Dockerfile -t ghcr.io/mathew), code:bash (bun run docker:pull:workspace), code:bash (bun run docker:build:workspace), code:bash (bun run docker:rebuild-workspaces), code:bash (bun run docker:rebuild-workspaces -- --dry-run), Editor acceptance smoke (+5 more) +Cohesion: 0.17 +Nodes (12): adminCookie, alice, aliceCookie, bob, bobCookie, body, carol, fakeContainerFor() (+4 more) ### Community 123 - "Catalog Integrity Test" -Cohesion: 0.18 -Nodes (14): Decision 038: PathPlanner integration, deployFileDeleteResponse(), deployFileWriteResponse(), Deploy-files contracts (zod schemas), deployFilePathSchema path traversal safety design, deployFilesSnapshotResponse(), apps/control/src/app/deploy-files.ts, apps/control/src/metrics.ts (templateRoute) (+6 more) +Cohesion: 0.15 +Nodes (11): attacker, classroom, coachRow, firstBody, firstCookie, firstSessions, firstWorkspace, school (+3 more) ### Community 124 - "Driver Station Page Object" -Cohesion: 0.2 -Nodes (13): hello-world bundled module, robot-starter bundled module, catalog/modules.json, docker() helper (Bun.spawn wrapper), openJavaFile(), java-tooling.spec.ts (docker smoke test), startWorkspace(), waitFor() polling helper (+5 more) +Cohesion: 0.28 +Nodes (11): config, db, log, AppliedMigrationRow, applyMigrations(), ensureMigrationTable(), listAppliedMigrations(), loadMigrations() (+3 more) + +### Community 125 - "Fake Socket Test Double" +Cohesion: 0.22 +Nodes (10): configMountType(), containerAttachedToNetwork(), containerHasPublishedPorts(), isLoopbackHost(), publishedPortFor(), v2LabelsMatch(), ContainerOrchestratorOptions, DockerInspectContainer (+2 more) ### Community 126 - "Session Hook & Heartbeat" -Cohesion: 0.17 -Nodes (11): FakeNt4Handle, FakeNt4Options, startFakeNt4(), aliceMessages, aliceReady, aliceSocket, allAlice, allBob (+3 more) +Cohesion: 0.15 +Nodes (12): 100 MB size cap, 6 imports per hour rate limit, Backup before import, Clone inside the container, not the host, Consequences, Context, Decision, Decision 016 — Project Import Strategy (+4 more) ### Community 127 - "Auto Choosers Hook Test" -Cohesion: 0.21 -Nodes (9): CodeStatus, CodeStatusPill(), TONES, codeStatusFromRun(), ConsoleLine(), ConsolePanel(), ConsolePanelProps, parseConsoleLine() (+1 more) +Cohesion: 0.15 +Nodes (12): 036 — Editor migration: openvscode-server → VSCodium reh-web, 1. VSCodium `reh-web`, not `code-server`, 2. Stay on the LinuxServer base image, 3. The editor keeps container port 3000, 4. Settings and extension paths do not move, 5. Workspace trust: diagnosed, not fixed, Consequences, Context (+4 more) ### Community 128 - "Command Injection Test Suite" Cohesion: 0.15 -Nodes (12): assetDir, assetsDir, config, cookie, createAssetZip(), formData, manifest, proc (+4 more) +Nodes (12): 009 - LSP reconnect, bridge serialization, and startup throttling, Context, Decision 1: Browser LSP client auto-reconnects with bounded backoff, Decision 2: Bridge serializes JDT LS spawns, Decision 3: Orchestrator-level LSP startup throttle, Decision 4: Cap proxy pending-message buffers, Decision 5: NT4 subprotocol mismatch is fail-fast, not silent, Decision 6: AS Lite in-iframe timeout banner (+4 more) ### Community 129 - "Default-Deny Route Coverage" -Cohesion: 0.17 -Nodes (12): adminCookie, alice, aliceCookie, bob, bobCookie, body, carol, fakeContainerFor() (+4 more) +Cohesion: 0.18 +Nodes (13): audit_log table, runtime_config table, audit log test suite, recordAuditEvent (audit.ts, tested), container concurrency cap test suite, max-active-containers cap persisted via runtime_config, adoption/restart gating, CapacityExceededError, ensureCodeContainer (containers) (+5 more) ### Community 130 - "Driver Station Page Object & Runtime Seeding" Cohesion: 0.15 -Nodes (12): 100 MB size cap, 6 imports per hour rate limit, Backup before import, Clone inside the container, not the host, Consequences, Context, Decision, Decision 016 — Project Import Strategy (+4 more) +Nodes (13): AdvantageScope git submodule pinned to release tag, Archive 001: Sim container architecture, Archive 002: AdvantageScope Lite hosted standalone, Archive 003: Minimal web shell, AS Lite GET /assets and /assets// routes, AS Lite window.location.hostname NT4 auto-detection, AS Lite embedded via iframe, not bundled/proxied, eclipse-temurin:17-jdk-jammy base image choice (+5 more) ### Community 131 - "Demo Mode Disk/Memory Limits" -Cohesion: 0.15 -Nodes (12): 036 — Editor migration: openvscode-server → VSCodium reh-web, 1. VSCodium `reh-web`, not `code-server`, 2. Stay on the LinuxServer base image, 3. The editor keeps container port 3000, 4. Settings and extension paths do not move, 5. Workspace trust: diagnosed, not fixed, Consequences, Context (+4 more) +Cohesion: 0.19 +Nodes (13): Decision 038: PathPlanner integration, deployFileDeleteResponse(), deployFileWriteResponse(), Deploy-files contracts (zod schemas), deployFilePathSchema path traversal safety design, deployFilesSnapshotResponse(), apps/control/src/app/deploy-files.ts, apps/control/src/metrics.ts (templateRoute) (+5 more) ### Community 132 - "Img Screenshots" -Cohesion: 0.15 -Nodes (12): 009 - LSP reconnect, bridge serialization, and startup throttling, Context, Decision 1: Browser LSP client auto-reconnects with bounded backoff, Decision 2: Bridge serializes JDT LS spawns, Decision 3: Orchestrator-level LSP startup throttle, Decision 4: Cap proxy pending-message buffers, Decision 5: NT4 subprotocol mismatch is fail-fast, not silent, Decision 6: AS Lite in-iframe timeout banner (+4 more) +Cohesion: 0.22 +Nodes (12): hello-world bundled module, robot-starter bundled module, catalog/modules.json, docker() helper (Bun.spawn wrapper), openJavaFile(), java-tooling.spec.ts (docker smoke test), startWorkspace(), waitFor() polling helper (+4 more) ### Community 133 - "Build Failure.spec" -Cohesion: 0.19 -Nodes (13): Archive 005: Java LSP MVP integration, Archive 006: Multi-tenancy spike findings, Archive 007: V1 sim container orchestration, Decision 032: Canonical Image Naming, Decision Logs README index, Docker labels as runtime source of truth, SQLite as cache, Dual-mode runtime provider (port mode / network mode), Local WPILib-aware JDT LS image (frc-lsp:mvp) (+5 more) +Cohesion: 0.15 +Nodes (12): apps/control/src/app/assets.ts, createPathPlannerDist(), DriverStationPage (page object), e2e/fixtures/gamepad-shim.ts, installGamepadShim(), pathplannerResponse(), e2e/fixtures/runtime.ts, seedWorkspaceProject() (+4 more) ### Community 134 - "Admin.po" -Cohesion: 0.18 -Nodes (8): SimRunAction, patchCall, postCall, { result }, updatedStatus, VALID_STATUS, useSimulationState(), UseSimulationStateReturn +Cohesion: 0.17 +Nodes (11): config, cookie, dataDir, fakeDocker, lease, name, now, passthrough (+3 more) ### Community 135 - "Java Tooling Smoke Test" -Cohesion: 0.26 -Nodes (11): ascopeRoot, assert(), createCatalogDir(), distDir, exists(), patchDir, repoRoot, runGit() (+3 more) - -### Community 136 - "Ws Bridge" Cohesion: 0.17 Nodes (9): args, argsPath, home, projectRoot, robotJar, sleeper, start, stop (+1 more) -### Community 137 - "Main" +### Community 136 - "Ws Bridge" Cohesion: 0.32 Nodes (11): Args, dirExists(), discoverWorkspaces(), fileExists(), main(), parseArgs(), restoreArchive(), restoreDb() (+3 more) +### Community 137 - "Main" +Cohesion: 0.26 +Nodes (11): ascopeRoot, assert(), createCatalogDir(), distDir, exists(), patchDir, repoRoot, runGit() (+3 more) + ### Community 138 - "Logging.test" Cohesion: 0.17 -Nodes (12): code:bash (curl -sS https://raw.githubusercontent.com/VSCodium/vscodium), code:markdown (| [openvscode-server](https://github.com/gitpod-io/openvscod), code:markdown (| [VSCodium](https://github.com/VSCodium/vscodium) / Code – ), code:markdown (- **`linuxserver/vscodium-web` container image** — GNU Gener), code:markdown (Merged per-student container for V2. Combines VSCodium reh-w), code:markdown (| Base image | linuxserver/vscodium-web:1.126.04524-ls35 | G), code:markdown (| 3000 | codium-server (HTTP + WebSocket) — overrides the ba), code:markdown (The container uses s6-overlay for process supervision. The u) (+4 more) +Nodes (12): code:typescript (test("s6 service script launches codium-server as primary pr), code:typescript (* In-process HTTP+WS server that impersonates the workspace ), code:bash (git add containers/code apps/control/src/__tests__ apps/cont), code:bash (cd /home/matt/dev/CodeRunner), code:bash (rm -rf containers/code/root/etc/s6-overlay/s6-rc.d/svc-openv), code:bash (find containers/code/root -type f | sort), code:block5 (containers/code/root/etc/s6-overlay/s6-rc.d/init-frc-setup/d), code:dockerfile (# V2 merged code container) (+4 more) ### Community 139 - "Metadata" Cohesion: 0.17 -Nodes (12): code:typescript (test("s6 service script launches codium-server as primary pr), code:typescript (* In-process HTTP+WS server that impersonates the workspace ), code:bash (git add containers/code apps/control/src/__tests__ apps/cont), code:bash (cd /home/matt/dev/CodeRunner), code:bash (rm -rf containers/code/root/etc/s6-overlay/s6-rc.d/svc-openv), code:bash (find containers/code/root -type f | sort), code:block5 (containers/code/root/etc/s6-overlay/s6-rc.d/init-frc-setup/d), code:dockerfile (# V2 merged code container) (+4 more) +Nodes (12): code:bash (curl -sS https://raw.githubusercontent.com/VSCodium/vscodium), code:markdown (| [openvscode-server](https://github.com/gitpod-io/openvscod), code:markdown (| [VSCodium](https://github.com/VSCodium/vscodium) / Code – ), code:markdown (- **`linuxserver/vscodium-web` container image** — GNU Gener), code:markdown (Merged per-student container for V2. Combines VSCodium reh-w), code:markdown (| Base image | linuxserver/vscodium-web:1.126.04524-ls35 | G), code:markdown (| 3000 | codium-server (HTTP + WebSocket) — overrides the ba), code:markdown (The container uses s6-overlay for process supervision. The u) (+4 more) ### Community 140 - "Middleware" Cohesion: 0.17 @@ -980,161 +984,161 @@ Cohesion: 0.17 Nodes (11): Automated Verification, code:block1 (bun run measure), code:bash (bun run typecheck), Comparison with V1, Decision, Decision 013: V2 Acceptance Pass, Host Capacity (10 students), Manual Verification (+3 more) ### Community 143 - "User Data" -Cohesion: 0.23 -Nodes (5): statusFromLease(), removeCodeContainer(), stopWorkspaceSim(), codeContainerName(), workspaceHomePath() +Cohesion: 0.18 +Nodes (12): Backup/restore flow removal, Bundled vs remote lesson catalog, Decision 016: Project Import Strategy, Decision 021: Testing Suite Implementation, Decision 022: Skip Docker Smoke and Import Tests, Decision 029: Lessons and Modules (referenced), Decision 038: Workspace Java Smoke (referenced), Docker smoke tier (skipped, broad) (+4 more) ### Community 144 - "Ws Proxy.spec" Cohesion: 0.21 Nodes (12): Container isolation (memory/disk/port caps), Container ports (3000/3300/5810), Decision 015: HALSim WebSocket Control Protocol, Decision 016: Imported Project Simulation Compatibility, Decision 018: Gamepad Input via HALSim WebSocket, Decision 019: Keyboard Input Mode, GamepadSessions class, Control-plane HALSim bridge (+4 more) ### Community 145 - "024 Container Memory Budget" -Cohesion: 0.17 -Nodes (12): AdvantageScope git submodule pinned to release tag, Archive 001: Sim container architecture, Archive 002: AdvantageScope Lite hosted standalone, Archive 003: Minimal web shell, AS Lite GET /assets and /assets// routes, AS Lite window.location.hostname NT4 auto-detection, AS Lite embedded via iframe, not bundled/proxied, eclipse-temurin:17-jdk-jammy base image choice (+4 more) +Cohesion: 0.21 +Nodes (12): Archive 005: Java LSP MVP integration, Archive 006: Multi-tenancy spike findings, Archive 007: V1 sim container orchestration, Decision Logs README index, Docker labels as runtime source of truth, SQLite as cache, Dual-mode runtime provider (port mode / network mode), Local WPILib-aware JDT LS image (frc-lsp:mvp), Loopback-only published NT4 ports (+4 more) ### Community 146 - "Halsim.test" -Cohesion: 0.25 -Nodes (3): FakeSocket, Listener, { result } +Cohesion: 0.3 +Nodes (6): Decision 015 — workspace routing / DS sync, Commit d111f70 (driver-station payload shape bug), e2e/fixtures/fake-halsim.ts, e2e/fixtures/fake-vscode.ts, Commit 158bab4 (hop-by-hop header stripping), seedRuntimeRunning() / seedWorkspaceProject() runtime fixture ### Community 147 - "Gamepad.test" -Cohesion: 0.22 -Nodes (9): PaneVisibility, readVisibility(), restored, { result }, { result, rerender }, { result, unmount }, UpperPanes, usePaneVisibility() (+1 more) - -### Community 148 - "Audit Prune" Cohesion: 0.24 Nodes (8): Env, isProxiedPath(), onRequest(), PagesFunctionContext, serviceUnavailable(), TOP_LEVEL_PROXIED, assetRequests, proxiedRequests +### Community 148 - "Audit Prune" +Cohesion: 0.25 +Nodes (3): FakeSocket, Listener, { result } + ### Community 149 - "Users" -Cohesion: 0.18 -Nodes (10): code:bash (git clone https://github.com/mathewdunne/CodeRunner coderunn), code:bash (cd website && bun install && bun run start), code:bash (bun run docs:dev), code:bash (bun run dev:control # Bun control plane on :4000 with --wa), code:bash (bun run verify # typecheck + Bun tests + Vitest + Pla), CodeRunner, Development, Documentation (+2 more) +Cohesion: 0.2 +Nodes (7): ListBlockDevicesOptions, listWorkspaceDiskLimitDevices(), log, IdleManagerOptions, log, getLogger(), devices ### Community 150 - "Helpers" Cohesion: 0.18 -Nodes (10): Build, code:bash (yarn), code:bash (yarn start), code:bash (yarn build), code:bash (USE_SSH=true yarn deploy), code:bash (GIT_USER= yarn deploy), Deployment, Installation (+2 more) +Nodes (10): code:bash (git clone https://github.com/mathewdunne/CodeRunner coderunn), code:bash (cd website && bun install && bun run start), code:bash (bun run docs:dev), code:bash (bun run dev:control # Bun control plane on :4000 with --wa), code:bash (bun run verify # typecheck + Bun tests + Vitest + Pla), CodeRunner, Development, Documentation (+2 more) ### Community 151 - "Card" Cohesion: 0.18 -Nodes (10): code:bash (git clone https://github.com/mathewdunne/CodeRunner coderunn), code:bash (CODERUNNER_DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) C), code:bash (CODERUNNER_DEMO_MODE=1 docker compose up), code:powershell ($env:CODERUNNER_DEMO_MODE = "1"; docker compose up), code:bat (set "CODERUNNER_DEMO_MODE=1" && docker compose up), code:bash (docker compose down --remove-orphans), Deploy for a team, Prerequisites (+2 more) +Nodes (10): Build, code:bash (yarn), code:bash (yarn start), code:bash (yarn build), code:bash (USE_SSH=true yarn deploy), code:bash (GIT_USER= yarn deploy), Deployment, Installation (+2 more) ### Community 152 - "Catalog.test" Cohesion: 0.18 -Nodes (10): Classroom Join Codes Implementation Plan, code:tsx (import { useCallback, useState } from "react";), code:bash (git add apps/web/src/admin), code:ts (/**), code:bash (bun run check:fix), File Structure, Global Constraints, Review Focus (+2 more) +Nodes (10): code:bash (git clone https://github.com/mathewdunne/CodeRunner coderunn), code:bash (CODERUNNER_DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) C), code:bash (CODERUNNER_DEMO_MODE=1 docker compose up), code:powershell ($env:CODERUNNER_DEMO_MODE = "1"; docker compose up), code:bat (set "CODERUNNER_DEMO_MODE=1" && docker compose up), code:bash (docker compose down --remove-orphans), Deploy for a team, Prerequisites (+2 more) ### Community 153 - "Block Devices" Cohesion: 0.18 -Nodes (10): Capacity limit, Classroom-density memory defaults, code:block1 (frc-sim.managed=true), Container labels, Container ports, First-run behavior, How student data persists, Idle auto-stop (+2 more) +Nodes (10): Classroom Join Codes Implementation Plan, code:tsx (import { useCallback, useState } from "react";), code:bash (git add apps/web/src/admin), code:ts (/**), code:bash (bun run check:fix), File Structure, Global Constraints, Review Focus (+2 more) ### Community 154 - "Ws Bridge" Cohesion: 0.18 -Nodes (10): Backup and Restore, Build, CLI Reference, Containerized ops: the `coderunner` CLI, Database, Docker Images and Containers, Docs Site, Quality and Tests (+2 more) +Nodes (10): Capacity limit, Classroom-density memory defaults, code:block1 (frc-sim.managed=true), Container labels, Container ports, First-run behavior, How student data persists, Idle auto-stop (+2 more) ### Community 155 - "Dropdown Menu" Cohesion: 0.18 -Nodes (10): code:block1 (https://github.com//), Constraints and limits, How to do an import, Importing a Team Project, Pushing and pulling after import, Running an imported project, Switching away discards the workspace, What a team import is for (+2 more) +Nodes (10): Backup and Restore, Build, CLI Reference, Containerized ops: the `coderunner` CLI, Database, Docker Images and Containers, Docs Site, Quality and Tests (+2 more) ### Community 156 - "020 Workspace Runtime Provider" Cohesion: 0.18 -Nodes (10): 017 — Migrate code container to linuxserver/openvscode-server, Bind mount target changes from /home/frc to /config, Context, Decisions, Layered s6-overlay: additive, not replacement, Migration Notes, Runtime PUID/PGID instead of build-time --user, Sim scripts remain independent of s6 (+2 more) +Nodes (10): code:block1 (https://github.com//), Constraints and limits, How to do an import, Importing a Team Project, Pushing and pulling after import, Running an imported project, Switching away discards the workspace, What a team import is for (+2 more) ### Community 157 - "030 Control Plane Hardening Pass" -Cohesion: 0.22 -Nodes (11): sendUpstreamWebSocketMessage, PROXY_PENDING_LIMIT, SocketData union type, createWebSocketHandlers, openProxyUpstream, resolveGamepadLease, Characterization-test-before-refactor pattern, websocket message router characterization tests (+3 more) +Cohesion: 0.18 +Nodes (10): 017 — Migrate code container to linuxserver/openvscode-server, Bind mount target changes from /home/frc to /config, Context, Decisions, Layered s6-overlay: additive, not replacement, Migration Notes, Runtime PUID/PGID instead of build-time --user, Sim scripts remain independent of s6 (+2 more) ### Community 158 - "Auth" -Cohesion: 0.2 -Nodes (11): defaultContainerUser, envContainerUser, loadControlConfig, parseHostDataDir, toHostPath(), selfInspect, Port mode vs network mode container orchestration, network-mode container orchestration test suite (+3 more) +Cohesion: 0.22 +Nodes (11): sendUpstreamWebSocketMessage, PROXY_PENDING_LIMIT, SocketData union type, createWebSocketHandlers, openProxyUpstream, resolveGamepadLease, Characterization-test-before-refactor pattern, websocket message router characterization tests (+3 more) ### Community 159 - "2026 08 30 Pathplanner Integration" Cohesion: 0.2 Nodes (11): container_leases table (V1 core schema), idx_container_leases_lsp_port_unique, idx_container_leases_sim_port_unique, container_leases.lsp_state column, container_leases.code_state column, V2 merged openvscode-server + sim image design, container_leases.vscode_container column, container_leases.vscode_port column (+3 more) ### Community 160 - "Xss.spec" -Cohesion: 0.2 -Nodes (11): CODE_DISK_READ_LIMIT (--device-read-bps cap), CODE_MEMORY_LIMIT raised to 4096m, /config as named volume in demo mode, Decision 028: Demo mode for zero-config local tryout, Decision 033: Workspace Disk Read Limit, Decision 034: Demo mode portability on Docker Desktop, 2026-07-07 disk-thrash production incident, group_add default CODERUNNER_DOCKER_GID=0 (+3 more) +Cohesion: 0.18 +Nodes (8): ControlApp in-process fixture / createApp(), Decision 018 — gamepad unplug safety, Decision 019 — keyboard focus, Decision 022 — skip Docker smoke / import tests, e2e/fixtures/fake-nt4.ts, Commit 95f450d (auto chooser stale fix), Commit cb9fea6 (gamepad selection persistence + no-lease), e2e/fixtures/gamepad-shim.ts ### Community 161 - "Build Ascope Lite" -Cohesion: 0.2 -Nodes (11): deploy-cloudflare job in deploy.yml, Decision 039: PathPlanner integration, Deploy-files API (/u/:slug/api/deploy-files/...), deploy.yml GitHub Actions workflow, GCE docker compose stack (control/caddy/alloy), PATHPLANNER_DIST_TAG pin, release.yml GitHub Actions workflow, pathplanner-dist.tar.gz packaging (+3 more) - -### Community 162 - "Index" Cohesion: 0.22 Nodes (11): ascope-iframe.spec.ts (T34.1 /scope route test), 001-lite-nt4-endpoint-injection.patch, embedded-mode NT4 endpoint injection mechanism, patches/advantagescope/README.md, runServerMessageSchema, file-permissions.spec.ts (T6.1 exec failure test), Decision 013: per-workspace NT4 isolation, nt4-multi-workspace.spec.ts (T35.1 NT4 isolation test) (+3 more) -### Community 163 - "Allowlist Management E2E" -Cohesion: 0.2 -Nodes (7): test (Playwright base.extend AppFixtures), apps/control/src/app.ts, apps/control/src/app/assets.ts, apps/control/src/config.ts (ControlConfig), createApp() (ControlApp factory), createPathPlannerDist(), pathplannerResponse() +### Community 162 - "Index" +Cohesion: 0.18 +Nodes (11): disk.tf (Terraform disk config), Hardware sizing guidance for team, FAQ, Why first build/run is slow, Offline capability of CodeRunner, Boot disk disposable, data disk precious, Seasonal Teardown, Manual data-disk snapshot (+3 more) + +### Community 164 - "Use Container Status.test" +Cohesion: 0.18 +Nodes (7): baBody, body, userRow, workspace, denied, GATED_PATHS, PUBLIC_PATHS ### Community 165 - "Frc Robot" Cohesion: 0.22 Nodes (8): SimButton(), SimButtonProps, SimControlsBlock(), SimControlsBlockProps, onRestart, onStart, onStop, TONE_CLASSES ### Community 166 - "Code Container Defaults.test" -Cohesion: 0.24 -Nodes (8): AllianceToggle(), AllianceToggleProps, Side, SIDE_ACTIVE, SIDE_EDGE, SIDE_STATION, sideOf(), onSelect +Cohesion: 0.29 +Nodes (9): stripHopByHopHeaders, isAllowedWebSocketOrigin(), isLoopbackHost(), log, normalizeHost(), admin allowlist CRUD, stripHopByHopHeaders RFC 7230 compliance tests, editor/HALSim proxy test suite (+1 more) ### Community 167 - "Java LSP Bridge (Archived)" -Cohesion: 0.31 -Nodes (3): GamepadSessions, halsimTarget(), resolveLease() - -### Community 168 - "2026 08 30 Pathplanner Integration" Cohesion: 0.2 Nodes (9): Arrange your workspace, Console lessons, Explore more, Get started, Joining a classroom, PathPlanner, Preview, Robot lessons and imported projects (+1 more) -### Community 169 - "Restore" +### Community 168 - "2026 08 30 Pathplanner Integration" Cohesion: 0.2 Nodes (9): code:ts (import { describe, expect, test } from "bun:test";), code:ts ({ path: "/admin/classrooms", expect: "deny" },), code:ts (import { rm } from "node:fs/promises";), code:ts (await deleteUserAndWorkspace(ctx, userId);), code:ts (import type { Database } from "bun:sqlite";), code:ts (export type AdminClassroomContext = {), code:ts (const classroomSweeper = new ClassroomSweeper({), code:bash (bun run check:fix && bun run typecheck) (+1 more) -### Community 170 - "Login.po" +### Community 169 - "Restore" Cohesion: 0.2 Nodes (9): 037 — Workspace cache aliases, extension pins, and trust, 1. Share the primed Gradle distribution with WPILib projects, 2. Prevent gallery resolution from replacing pinned VSIXs, 3. Disable workspace trust in the hosted workbench, 4. Keep Gradle daemon limits out of editor build arguments, 5. Close the workspace-image build follow-ups, Browser-owned settings finding, code:text (permwrapper -> wrapper) (+1 more) -### Community 171 - "Default Deny.test" +### Community 170 - "Login.po" Cohesion: 0.2 Nodes (9): 1. Why not Better Auth `testUtils`, 2. Auth-callback path tests are deferred, 3. Browser-heavy specs use `test.fixme`, not deletion, 4. HTTP-driven specs preferred over DOM-driven specs where possible, 5. Decisions on smaller details, Decision 021: Testing suite implementation — deviations from TESTING-PLAN.md, Files Touched, Future Work (Deferred) (+1 more) -### Community 172 - "Auth Demo.test" +### Community 171 - "Default Deny.test" Cohesion: 0.2 Nodes (9): Decision 018: Gamepad Input via HALSim WebSocket, Files Touched, Future Work (Deferred), Safety: disable on disconnect, Summary, Why a dedicated WebSocket from browser to control plane, Why a single controller on port 0 for v1, Why HALSim WS, not the FRC DS UDP protocol (+1 more) -### Community 173 - "Robot" +### Community 172 - "Auth Demo.test" Cohesion: 0.2 Nodes (9): Alternatives considered, App-side stays vendor-neutral, code:json ({"timestamp":"2026-05-21T14:23:01.482Z","level":"info","cate), Consequences, Context, Decision, Decision 027: Ship control-plane logs to Grafana Cloud Loki, Label cardinality (+1 more) -### Community 174 - "Cleanup Containers" +### Community 173 - "Robot" Cohesion: 0.2 Nodes (9): Base image: `gitpod/openvscode-server:1.105.1`, Consequences, Context, Decision, Decision 012: V2 Code Image — Base Image and Extension Strategy, Direct launch base path handling, Extension cache seeding pattern, Extensions: download at build time (+1 more) -### Community 175 - "Config" +### Community 174 - "Cleanup Containers" Cohesion: 0.2 Nodes (9): 030 — Control-Plane Hardening Pass, Consequences, Context, Correctness fixes, Decision, Performance, Security, Status (+1 more) -### Community 176 - "Use Gamepad" +### Community 175 - "Config" Cohesion: 0.2 Nodes (9): 004 - Backend wiring for save and run, Context, Custom WebSocket sender, no new dependency, Decisions, Host backend plus Docker CLI, Minimal endpoints and run protocol, One-command dev stack without Docker Compose, Replaceable sim process inside long-lived container (+1 more) -### Community 177 - "016 Project Import Strategy" +### Community 176 - "Use Gamepad" Cohesion: 0.2 Nodes (9): 007 - V1 sim container orchestration, code:text (frc-sim.managed=true), Context, Decisions, Docker labels are adopted back into SQLite, Lazy ensure, visible status, Loopback-only published ports, Runtime cache seed (+1 more) -### Community 178 - "[[path]]" +### Community 177 - "016 Project Import Strategy" Cohesion: 0.2 Nodes (9): 008 - V1 LSP container and Bun-native bridge, Browser LSP client extended for multi-file projects, Bun-native bridge instead of `vscode-ws-jsonrpc`, code:block1 (data/users//project -> /workspace/project), `container_leases` lease state split, Context, Decisions, Generic `ContainerOrchestrator` (+1 more) -### Community 179 - "Clean" +### Community 178 - "[[path]]" Cohesion: 0.2 Nodes (9): Choosing a document, If a report looks wrong, Plain-Java lessons, Reading Documents (Preview), Refresh, Refreshing a document, Supported files, Things it deliberately does not do (+1 more) -### Community 180 - "Robot Container" +### Community 179 - "Clean" Cohesion: 0.24 -Nodes (10): Admin workspace backup action, Admin workspace restore action, createProjectArchive, restoreProjectArchive, runTar, isInsideDirectory, deployFileDeleteResponse, deployFileWriteResponse (+2 more) +Nodes (10): run_jobs table (V1 core schema), sessions table (V1 core schema), users table (V1 core schema), workspaces table (V1 core schema), container_leases.halsim_port column, Better Auth migration: drop pre-OAuth auth model, container_leases_new table (Better Auth rebuild), run_jobs_new table (Better Auth rebuild) (+2 more) -### Community 181 - "Dist Download Utility" -Cohesion: 0.24 -Nodes (10): container_leases.halsim_port column, container_leases_new table (Better Auth rebuild), HalSimBridge (halsim.ts), HalSimBridge test suite, createFakeDocker, dockerInspect (fixture builder), Nt4AutoChooserBridge, Superseded run must not clobber newer run's status (+2 more) +### Community 180 - "Robot Container" +Cohesion: 0.27 +Nodes (10): workspaces.current_module column, First-login empty workspace (no template seed) design (Decision 029/D7), BundledCatalogSource (catalog.ts), CatalogSource interface, createCatalogSource (catalog.ts), parseCatalogRepo (catalog.ts), RemoteCatalogSource (catalog.ts), Two-source lesson catalog pattern (bundled + remote) (+2 more) -### Community 182 - "Cloudflare Pages Function.test" +### Community 181 - "Dist Download Utility" Cohesion: 0.2 Nodes (10): readError, useAutoChoosers test suite, useAutoChoosers, useContainerStatus test suite, useContainerStatus, probeEditor, useEditorReachability test suite, useEditorReachability (+2 more) +### Community 182 - "Cloudflare Pages Function.test" +Cohesion: 0.22 +Nodes (10): CODE_DISK_READ_LIMIT (--device-read-bps cap), CODE_MEMORY_LIMIT raised to 4096m, /config as named volume in demo mode, Decision 028: Demo mode for zero-config local tryout, Decision 033: Workspace Disk Read Limit, Decision 034: Demo mode portability on Docker Desktop, 2026-07-07 disk-thrash production incident, group_add default CODERUNNER_DOCKER_GID=0 (+2 more) + ### Community 183 - "PathPlanner Integration" Cohesion: 0.2 Nodes (10): code-server --abs-proxy-base-path (rejected), Decision 011: V2 editor spike, Decision 017: LinuxServer base migration, Decision 026: Editor Default Theme, Decision 036: Editor migration openvscode-server to VSCodium reh-web, Machine settings.json theme seeding, --user-data-dir ignored by codium-server (post-review correction), VSCodium reh-web chosen over code-server (+2 more) @@ -1144,324 +1148,332 @@ Cohesion: 0.22 Nodes (10): Decision 037: Workspace cache aliases, extension pins, and trust, Decision 038: Java tooling compatibility, extension reconciliation, and real-image smoke, --disable-workspace-trust server flag, --do-not-include-pack-dependencies flag, Separate tooling (JDT, Java 21) and project (Java 17) JDKs, bun run e2e:workspace-java real-container smoke, Managed extension reconciliation on every container start, Gradle daemon JVM args rejected by Tooling API (+2 more) ### Community 185 - "Metrics.test" +Cohesion: 0.31 +Nodes (10): Grafana Alloy config template (config.alloy.tmpl), Alloy log pipeline bounded active-series design, bootstrap.sh first-boot provisioning script, Caddyfile (GCE deployment), Decision 031 (containerized control plane), GCE cloud-init user-data.yaml, deploy/ directory README, docker-compose.prod.yml (Caddy/Alloy overlay) (+2 more) + +### Community 186 - "004 Backend Wiring" +Cohesion: 0.2 +Nodes (10): Decision 041: Project Preview, Driver Station Auto Tab, Console Lessons, Console Tab, Driver Station Controls Tab, Driver Station, build/reports/** Generated Documents, README.md Auto-Open in Preview (+2 more) + +### Community 187 - "Store" Cohesion: 0.22 Nodes (10): Decision 039: PathPlanner Integration, Decision 042: Collapsible Workspace Panes, createAdvantageScopeDist / createPathPlannerDist Throwaway Dists, apps/control/src/__tests__/helpers.ts, AdvantageScope Tab, Choreo Files Read-Only Visibility, User Menu Layout Control, PathPlanner Deploy Path (src/main/deploy/pathplanner) (+2 more) -### Community 186 - "004 Backend Wiring" +### Community 188 - "Dist Download" Cohesion: 0.31 Nodes (5): connectGamepad(), installGamepadShim(), setGamepadAxes(), consoleErrors, leaseErrors -### Community 187 - "Store" +### Community 189 - "Img Screenshots" Cohesion: 0.22 Nodes (7): catalog, catalogRoot, ids, manifestPath, repoRoot, subdirPath, lessonCatalogSchema -### Community 188 - "Dist Download" +### Community 190 - "Audit/Reconciliation Test Suite" Cohesion: 0.25 Nodes (6): patchCall, { result }, updatedResponse, VALID_RESPONSE, useAutoChoosers(), UseAutoChoosersReturn -### Community 189 - "Img Screenshots" +### Community 191 - "Dialog" Cohesion: 0.25 Nodes (6): LoadState, heartbeatCalls, { result }, { unmount }, VALID_SESSION, useSession() -### Community 190 - "Audit/Reconciliation Test Suite" -Cohesion: 0.22 -Nodes (8): bridge, enabledDisable, flush, joystick, messages, msg, sockets, zeroButtons - -### Community 191 - "Dialog" -Cohesion: 0.22 -Nodes (4): IdleManager, IdleManagerOptions, log, getLogger() - ### Community 192 - "Main" -Cohesion: 0.22 -Nodes (7): code:ts (import { describe, expect, test } from "bun:test";), code:ts (export type BunUpgradeServer = {), code:ts (/**), code:ts (import type {), code:ts (/** Stop a workspace's run and container and drop its live b), code:bash (bun run check:fix && bun run typecheck), Task 4: Join dispatcher — rate limit, demo gate, retiring the previous session, `/join` shell +Cohesion: 0.39 +Nodes (8): Args, backupDatabase(), dirExists(), fileExists(), main(), parseArgs(), runTar(), timestamp() ### Community 193 - "Main" Cohesion: 0.22 -Nodes (8): code:ts (describe("guest workspace routes", () => {), code:ts ({ path: "/u/alice/api/leave", method: "POST", expect: "deny"), code:ts (/** Present for classroom guests (decision 043). */), code:ts (/** Stops a workspace's run and container; files are kept. *), code:ts (const classroom = findGuestClassroom(storage.db, auth.user.i), code:ts (// Classroom guests click "Leave" so the next student at the), code:bash (bun run check:fix && bun run typecheck), Task 6: Leave endpoint and guest info on the session response +Nodes (8): bashCalls, cloneCall, ctx, importer, mock, row, workspace, GithubImportContext ### Community 194 - "Typecheck" Cohesion: 0.22 -Nodes (8): 038 — Java tooling compatibility, extension reconciliation, and real-image smoke, Add a targeted real-container smoke, Consequences, Context, Decision, Keep separate tooling and project JDKs, Pin a compatible Java extension matrix, Reconcile managed extensions on every container start +Nodes (8): code:ts (describe("guest workspace routes", () => {), code:ts ({ path: "/u/alice/api/leave", method: "POST", expect: "deny"), code:ts (/** Present for classroom guests (decision 043). */), code:ts (/** Stops a workspace's run and container; files are kept. *), code:ts (const classroom = findGuestClassroom(storage.db, auth.user.i), code:ts (// Classroom guests click "Leave" so the next student at the), code:bash (bun run check:fix && bun run typecheck), Task 6: Leave endpoint and guest info on the session response ### Community 195 - "Project Preview & Right Pane" Cohesion: 0.22 -Nodes (8): Alternatives considered, Auth, Cardinality discipline, Consequences, Context, Decision, Decision 023: Metrics and observability via Prometheus + Grafana Cloud, Status +Nodes (7): code:ts (import { describe, expect, test } from "bun:test";), code:ts (export type BunUpgradeServer = {), code:ts (/**), code:ts (import type {), code:ts (/** Stop a workspace's run and container and drop its live b), code:bash (bun run check:fix && bun run typecheck), Task 4: Join dispatcher — rate limit, demo gate, retiring the previous session, `/join` shell ### Community 196 - "Global Setup" -Cohesion: 0.28 -Nodes (9): run_jobs table (V1 core schema), sessions table (V1 core schema), users table (V1 core schema), workspaces table (V1 core schema), Better Auth migration: drop pre-OAuth auth model, run_jobs_new table (Better Auth rebuild), workspaces_new table (Better Auth rebuild), workspaces.current_module_kind column (+1 more) +Cohesion: 0.22 +Nodes (8): 038 — Java tooling compatibility, extension reconciliation, and real-image smoke, Add a targeted real-container smoke, Consequences, Context, Decision, Keep separate tooling and project JDKs, Pin a compatible Java extension matrix, Reconcile managed extensions on every container start ### Community 197 - "Docusaurus.config" Cohesion: 0.22 -Nodes (9): GATED_PATHS list, PUBLIC_PATHS list, Default-deny auth coverage via explicit route manifest (no route table introspection), default-deny route coverage test suite (Plan §A.7.6), PathPlanner subtree writable, choreo subtree read-only design, safeRelativeAssetPath (asset path validation), Symlink and path-traversal defense for deploy-files write/delete, deploy-files snapshot/write/delete test suite (+1 more) +Nodes (8): Alternatives considered, Auth, Cardinality discipline, Consequences, Context, Decision, Decision 023: Metrics and observability via Prometheus + Grafana Cloud, Status ### Community 198 - "Sidebars" -Cohesion: 0.25 -Nodes (9): Backup/restore flow removal, Bundled vs remote lesson catalog, Decision 021: Testing Suite Implementation, Decision 022: Skip Docker Smoke and Import Tests, Decision 029: Lessons and Modules (referenced), Decision 038: Workspace Java Smoke (referenced), Docker smoke tier (skipped, broad), Lessons-Design.md (D1-D13) (+1 more) +Cohesion: 0.22 +Nodes (7): Canonical registry-qualified image names, Decision 032: Canonical Image Naming, [command, kind], dockerfiles, image, Kind, subprocess ### Community 199 - "Web App Shell & Theming" -Cohesion: 0.22 -Nodes (9): Decision 041: Project Preview, Driver Station Auto Tab, Console Lessons, Console Tab, Driver Station Controls Tab, Driver Station, build/reports/** Generated Documents, README.md Auto-Open in Preview (+1 more) +Cohesion: 0.31 +Nodes (9): Code container VS Code defaults test suite, containers/code/Dockerfile, codium-server (VSCodium reh-web), Decision 033: workspace disk read limit, containers/code/root/.../init-frc-setup/run, robot-starter .vscode/settings.json, svc-vscodium-web/run service script, Why codium-server over code-server: --server-base-path support (+1 more) -### Community 201 - "Vite.config" -Cohesion: 0.29 -Nodes (6): PathPlannerPane, PathPlannerPaneProps, first, frame, { rerender }, second +### Community 200 - "Constants" +Cohesion: 0.28 +Nodes (4): createApp(), RunManager (run job lifecycle), makeScriptedRunCommandFactory(), MockWorkspaceRuntimeProvider -### Community 202 - "Audit/Reconciliation Test Suite" +### Community 201 - "Vite.config" Cohesion: 0.25 -Nodes (7): CodeRunner, How to use CodeRunner, Lessons and team projects, Next steps, Self-hosted and modest to run, Video walkthrough, What students get +Nodes (6): cookie, firstBody, rows, runtime2, secondBody, states ### Community 203 - "Icon Rail" -Cohesion: 0.25 -Nodes (8): code:tsx (import { render, screen, waitFor } from "@testing-library/re), code:tsx (interface UserMenuProps {), code:tsx (function formatEndsAt(iso: string): string {), code:tsx (/{project,assets} backup layout, scripts/allowlist.ts (allowlist CLI), scripts/audit-prune.ts, scripts/backup.ts -### Community 241 - "Dropdown Menu" +### Community 239 - "Dropdown Menu" Cohesion: 0.38 Nodes (5): container_leases table, clearContainerLeases(), rebuildWorkspaces(), rebuildWorkspaces test suite, Grafana ops dashboard screenshot: host VM, workspaces, runs, control-plane panels -### Community 242 - "Dropdown Menu" +### Community 240 - "Dropdown Menu" Cohesion: 0.33 Nodes (7): PathPlanner Pane Overview Screenshot, Sign-In Page Screenshot, Switch Project Dialog Screenshot, Team Import Progress Screenshot, Using CodeRunner - Ready State Screenshot, Using CodeRunner - Start State Screenshot, Workspace Shell Three-Pane Layout Screenshot -### Community 243 - "Dropdown Menu" +### Community 241 - "Dropdown Menu" Cohesion: 0.29 Nodes (5): e2e/fixtures/auth.ts (loginAs helper), Better Auth cookie URL-encoding consistency, loginAs(), LoginPage (page object), signToken() (HMAC session signing) -### Community 244 - "Dropdown Menu" +### Community 242 - "Dropdown Menu" +Cohesion: 0.38 +Nodes (7): ControlApp, fake-halsim.ts, fake-vscode.ts, Fixture Architecture (In-Process ControlApp), loginAs (Seeded Auth Fixture), MockWorkspaceRuntimeProvider, runtime.ts Fixture Helpers + +### Community 243 - "Dropdown Menu" Cohesion: 0.4 Nodes (4): { result }, { unmount }, VALID_STATUS, useContainerStatus() -### Community 245 - "Dropdown Menu" +### Community 244 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): dockerfile, initScript, repoRoot, robotSettings, settings +### Community 245 - "Dropdown Menu" +Cohesion: 0.33 +Nodes (6): code:tsx (import { render, screen, waitFor } from "@testing-library/re), code:tsx (import { type FormEvent, useState } from "react";), code:tsx ({), code:tsx ({/* Classroom guests */}), code:bash (bun run check:fix && bun run typecheck), Task 7: `/join` page and login link + ### Community 246 - "Dropdown Menu" Cohesion: 0.33 Nodes (6): code:bash (git checkout -b classroom-join-codes), code:ts (import { describe, expect, test } from "bun:test";), code:sql (-- Classroom join codes (decision 043). Guest users point at), code:ts (/**), code:bash (bun run check:fix && bun run typecheck), Task 1: Classroom core module and migration ### Community 247 - "Dropdown Menu" Cohesion: 0.33 -Nodes (6): code:tsx (import { render, screen, waitFor } from "@testing-library/re), code:tsx (import { type FormEvent, useState } from "react";), code:tsx ({), code:tsx ({/* Classroom guests */}), code:bash (bun run check:fix && bun run typecheck), Task 7: `/join` page and login link +Nodes (5): Background — verified findings, Follow-up disposition (closed 2026-08-26), Global Constraints, Out of scope, VSCodium-web Editor Migration Implementation Plan ### Community 248 - "Dropdown Menu" Cohesion: 0.33 -Nodes (5): Background — verified findings, Follow-up disposition (closed 2026-08-26), Global Constraints, Out of scope, VSCodium-web Editor Migration Implementation Plan +Nodes (6): code:bash (git add ), code:bash (bun run docs:build && grep -rl "vscodium-web-migration" webs), code:bash (git add docs/decisions/036-vscodium-web-migration.md docs/de), File Structure, Task 5: Full regression gate, Task 7: Record decision log 036 ### Community 249 - "Resizable" Cohesion: 0.33 -Nodes (6): code:bash (git add ), code:bash (bun run docs:build && grep -rl "vscodium-web-migration" webs), code:bash (git add docs/decisions/036-vscodium-web-migration.md docs/de), File Structure, Task 5: Full regression gate, Task 7: Record decision log 036 +Nodes (6): code:bash (docker exec cr-smoke sh -c 'cat /config/data/User/settings.j), code:bash (| (if $mode == "project" then . else ."security.workspace.tr), code:bash (bun run docker:build:workspace), code:bash (git add containers/code/root/etc/s6-overlay/s6-rc.d/init-frc), code:bash (docker rm -f cr-smoke), Task 4: Verify Java reaches Standard Mode; fix workspace trust only if it does not ### Community 250 - "Resizable" Cohesion: 0.33 -Nodes (6): code:bash (docker exec cr-smoke sh -c 'cat /config/data/User/settings.j), code:bash (| (if $mode == "project" then . else ."security.workspace.tr), code:bash (bun run docker:build:workspace), code:bash (git add containers/code/root/etc/s6-overlay/s6-rc.d/init-frc), code:bash (docker rm -f cr-smoke), Task 4: Verify Java reaches Standard Mode; fix workspace trust only if it does not +Nodes (5): 020 - Workspace runtime provider boundary, Consequences, Context, Decision, Status ### Community 251 - "Separator" Cohesion: 0.33 -Nodes (5): 020 - Workspace runtime provider boundary, Consequences, Context, Decision, Status +Nodes (5): 024 — Bound Per-Workspace Container Memory, Consequences, Context, Decision, Status ### Community 252 - "Sonner" Cohesion: 0.33 -Nodes (5): 024 — Bound Per-Workspace Container Memory, Consequences, Context, Decision, Status +Nodes (5): 035 — Multi-Arch Images and CI/Release/Deploy Workflow Split, Consequences, Context, Decision, Status ### Community 253 - "Tabs" Cohesion: 0.33 -Nodes (5): 035 — Multi-Arch Images and CI/Release/Deploy Workflow Split, Consequences, Context, Decision, Status +Nodes (5): 034 — Demo mode portability on Docker Desktop, Consequences, Context, Decision, What was deliberately not done ### Community 254 - "Tabs" Cohesion: 0.33 -Nodes (5): 034 — Demo mode portability on Docker Desktop, Consequences, Context, Decision, What was deliberately not done +Nodes (5): 025 — Detach the Simulation JVM from Gradle, Consequences, Context, Decision, Status ### Community 255 - "Tabs" -Cohesion: 0.33 -Nodes (5): 025 — Detach the Simulation JVM from Gradle, Consequences, Context, Decision, Status +Cohesion: 0.53 +Nodes (6): AdvantageKit Logger, robot-starter catalog lesson module, robot-starter Constants.java template class, robot-starter Main.java template class, robot-starter Robot.java template class, robot-starter RobotContainer.java template class ### Community 256 - "Tabs" Cohesion: 0.4 -Nodes (6): slugFromEmail, login, routing and shell APIs test suite, slugFromEmail property tests (P4-P6), ensureWorkspaceFiles, ensureWorkspaceForUser (slug collision suffixing) +Nodes (6): Decision 039: PathPlanner integration, Deploy-files API (/u/:slug/api/deploy-files/...), pathplanner-dist.tar.gz packaging, PathPlanner (upstream), PathPlanner topbar tab (iframe beside AdvantageScope), pathplanner-web fork (mathewdunne/pathplanner-web) ### Community 257 - "Tabs" -Cohesion: 0.53 -Nodes (6): AdvantageKit Logger, robot-starter catalog lesson module, robot-starter Constants.java template class, robot-starter Main.java template class, robot-starter Robot.java template class, robot-starter RobotContainer.java template class +Cohesion: 0.33 +Nodes (6): Decision 010: Gradle Project Cache Isolation for Sim and LSP, --project-cache-dir $HOME/.gradle-project-sim, stop-sim.sh, V1-10 three-user smoke test, non-destructive Gradle headless override (strips GUI, enables WS server), robot lesson kind -### Community 261 - "Tooltip" +### Community 260 - "Tooltip" +Cohesion: 0.4 +Nodes (4): DriverStation(), change, { container, rerender }, props + +### Community 262 - "Use Run Channel" Cohesion: 0.5 Nodes (4): dryRun, main(), _repoRoot, run() -### Community 263 - "Image" -Cohesion: 0.4 -Nodes (5): code:bash (# Seed Gradle cache on first run.), code:bash (# Fix ownership for /config and /workspace (linuxserver conv), code:bash (# Fix ownership of what this script created as root. The bas), code:bash (git add containers/code/root/etc/s6-overlay/s6-rc.d/init-frc), Task 2: Scope `init-frc-setup`'s ownership pass to what it actually creates - ### Community 264 - "Verify Ascope" Cohesion: 0.4 -Nodes (4): Consequences, Context, Decision, Decision 014 — Better Auth Integration +Nodes (5): code:bash (# Seed Gradle cache on first run.), code:bash (# Fix ownership for /config and /workspace (linuxserver conv), code:bash (# Fix ownership of what this script created as root. The bas), code:bash (git add containers/code/root/etc/s6-overlay/s6-rc.d/init-frc), Task 2: Scope `init-frc-setup`'s ownership pass to what it actually creates ### Community 265 - "Community 265" Cohesion: 0.4 -Nodes (4): 029 — Lessons & Modules, Consequences, Context, Implementation decisions +Nodes (4): Consequences, Context, Decision, Decision 014 — Better Auth Integration ### Community 266 - "Community 266" Cohesion: 0.4 -Nodes (4): 040 — SELinux container mounts, Consequences, Context, Decision +Nodes (4): 029 — Lessons & Modules, Consequences, Context, Implementation decisions ### Community 267 - "Community 267" Cohesion: 0.4 -Nodes (4): Consequences, Context, Decision, Decision 019: Keyboard Input Mode +Nodes (4): 040 — SELinux container mounts, Consequences, Context, Decision ### Community 268 - "Community 268" Cohesion: 0.4 -Nodes (4): 039 — PathPlanner integration, Consequences, Context, Decision +Nodes (4): Consequences, Context, Decision, Decision 019: Keyboard Input Mode ### Community 269 - "Community 269" Cohesion: 0.4 -Nodes (4): 010 - Gradle project cache isolation for sim and LSP, Context, Decisions, Implications +Nodes (4): 039 — PathPlanner integration, Consequences, Context, Decision ### Community 270 - "Community 270" Cohesion: 0.4 -Nodes (5): Decision 018 (gamepad unplug safety), listConnectedGamepads, makeLabel, useGamepad test suite, useGamepad +Nodes (4): 010 - Gradle project cache isolation for sim and LSP, Context, Decisions, Implications ### Community 271 - "Community 271" Cohesion: 0.4 -Nodes (5): Decision 016: Project Import Strategy, Decision 017: linuxserver Base Migration, git clone via docker exec inside container, linuxserver/openvscode-server base image, Project import strategy (clone-in-container, strip .git, backup) +Nodes (5): Decision 018 (gamepad unplug safety), listConnectedGamepads, makeLabel, useGamepad test suite, useGamepad ### Community 272 - "Community 272" Cohesion: 0.4 @@ -1493,24 +1505,24 @@ Nodes (3): Active (V2 and post-V2), Archive, Decision Logs ### Community 280 - "Community 280" Cohesion: 0.5 -Nodes (3): Robot Starter, Running it, Where the code lives +Nodes (3): 044 — Auth0 SSO, Decision, Validation ### Community 281 - "Community 281" Cohesion: 0.5 -Nodes (3): Hello, World, Running it, Try stuff +Nodes (3): Robot Starter, Running it, Where the code lives ### Community 282 - "Community 282" +Cohesion: 0.5 +Nodes (3): Hello, World, Running it, Try stuff + +### Community 283 - "Community 283" Cohesion: 0.67 Nodes (4): defaultLogFormat, formatRecordJson, parseLogFormatEnv, logging.ts test suite (formatRecordJson, parseLogFormatEnv, defaultLogFormat) -### Community 283 - "Community 283" +### Community 284 - "Workspace Container Bind-Mount & Memory Bounds" Cohesion: 0.5 Nodes (4): Route templating for bounded metric cardinality, statusClass, templateRoute, metrics.ts test suite (templateRoute, statusClass) -### Community 284 - "Workspace Container Bind-Mount & Memory Bounds" -Cohesion: 0.67 -Nodes (4): GamepadSessions (gamepad.ts), Gamepad release/close safety-disables joystick and driver station, GamepadSessions test suite, HalSimBridgeUnavailableError (halsim.ts) - ### Community 285 - "Community 285" Cohesion: 0.67 Nodes (4): persist middleware with partialize (inputMode only), UI store test suite, UIState interface, useUIStore Zustand store @@ -1527,21 +1539,21 @@ Nodes (4): downloadAndExtract(), withScratch(), fetch-dist main(), fetchPathPlan Cohesion: 0.5 Nodes (4): Demo mode landing screenshot: CodeRunner IDE with demo-mode banner, Driver Station workbench screenshot: sim controls, mode, console output, Lesson catalog / Switch project modal screenshot, Lesson README opened screenshot: editor + AdvantageScope + README preview -### Community 293 - "Community 293" -Cohesion: 0.67 -Nodes (3): Auto-chooser NT4 bridge msgpack decoding, auto-chooser NT4 bridge test, encodeMsgPack test helper +### Community 289 - "Community 289" +Cohesion: 0.5 +Nodes (4): AdvantageScope Lite NT4 client, e2e/fixtures/fake-nt4.ts, NT4 wire protocol: MessagePack binary + JSON text, startFakeNt4() ### Community 294 - "Community 294" Cohesion: 0.67 -Nodes (3): Badge, KindTag, SwitchProjectDialog +Nodes (3): Auto-chooser NT4 bridge msgpack decoding, auto-chooser NT4 bridge test, encodeMsgPack test helper ### Community 295 - "Community 295" Cohesion: 0.67 -Nodes (3): Button, DialogContent, DialogFooter +Nodes (3): Badge, KindTag, SwitchProjectDialog ### Community 296 - "Community 296" Cohesion: 0.67 -Nodes (3): Classroom-density memory defaults, Decision 024: Container Memory Budget, VS Code Gradle Build Server path +Nodes (3): Button, DialogContent, DialogFooter ### Community 297 - "Community 297" Cohesion: 0.67 @@ -1566,9 +1578,9 @@ Nodes (3): scripts/apply-ascope-patches.ts, scripts/build-ascope-lite.ts, script docs/decisions/README.md · relation: conceptually_related_to ## Knowledge Gaps -- **2207 isolated node(s):** `workspace`, `frame`, `victimWorkspace`, `attackerWorkspace`, `console` (+2202 more) +- **2220 isolated node(s):** `workspace`, `frame`, `victimWorkspace`, `attackerWorkspace`, `console` (+2215 more) These have ≤1 connection - possible missing edges or undocumented components. -- **90 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. +- **87 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ diff --git a/graphify-out/graph.html b/graphify-out/graph.html index 03a9077d..cb97c946 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -61,12 +61,12 @@

Communities

-
4648 nodes · 6821 edges · 383 communities
+
4675 nodes · 6859 edges · 383 communities