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 e3730468..9e2b4c6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,12 @@ session. Guest sessions die with the classroom (checked in container, and `ClassroomSweeper` deletes guests of ended/expired classrooms. See `docs/decisions/043-classroom-join-codes.md`. +**Auth0 SSO (post-V2):** optional third login provider (`AUTH0_*` env) via +Better Auth's `genericOAuth`. Auth0 users skip the allowlist; their role comes +from an ID-token roles claim, re-read at every sign-in. Users with no role are +rejected in `mapProfileToUser` (after-hook throws are lost behind the callback +redirect). See `docs/decisions/044-auth0-sso.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 + @@ -145,7 +151,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–043 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..118b56e7 100644 --- a/apps/control/src/__tests__/auth.test.ts +++ b/apps/control/src/__tests__/auth.test.ts @@ -278,7 +278,7 @@ describe("bootstrap admin (CODERUNNER_ADMIN_EMAIL)", () => { ); }); - test("startup seeding is idempotent, promotes existing students, leaves admins", async () => { + test("startup seeding is idempotent, promotes existing students, leaves admins and Auth0 users", async () => { const root = await mkdtemp(join(tmpdir(), "frc-bootstrap-")); try { const catalogDir = await createCatalogDir(root); @@ -294,7 +294,7 @@ describe("bootstrap admin (CODERUNNER_ADMIN_EMAIL)", () => { sessionSecret: "test-session-secret", baseUrl: "http://localhost:4000", containerAutoStart: false, - adminEmails: ["coach@team.org", "boss@team.org"], + adminEmails: ["coach@team.org", "boss@team.org", "lead@team.org"], }; // First startup: both admin emails are seeded into the allowlist. @@ -302,7 +302,11 @@ describe("bootstrap admin (CODERUNNER_ADMIN_EMAIL)", () => { const afterFirst = JSON.parse(await readFile(allowlistPath, "utf8")) as { emails: string[]; }; - expect(afterFirst.emails).toEqual(["boss@team.org", "coach@team.org"]); + expect(afterFirst.emails).toEqual([ + "boss@team.org", + "coach@team.org", + "lead@team.org", + ]); // Simulate accounts that already exist: a coach who signed in as a // student before the env was set (with the mixed-case email the OAuth @@ -334,6 +338,32 @@ describe("bootstrap admin (CODERUNNER_ADMIN_EMAIL)", () => { "admin", "boss", ); + // An Auth0 student holding an admin address. Auth0 sets the role at + // every sign-in (admin only for a verified address), so startup must + // not promote it: the address may be an unverified self-sign-up. + insertUser.run( + "userLeadCCCCCCCCCCCC", + "Lead", + "lead@team.org", + 0, + null, + staleAdminTimestamp, + staleAdminTimestamp, + "student", + "lead", + ); + first.storage.db + .query( + "INSERT INTO account (id, accountId, providerId, userId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)", + ) + .run( + "acctLeadCCCCCCCCCCCC", + "auth0|lead", + "auth0", + "userLeadCCCCCCCCCCCC", + staleAdminTimestamp, + staleAdminTimestamp, + ); first.close(); // Second startup on the same data dir: idempotent allowlist, promotes the @@ -343,7 +373,11 @@ describe("bootstrap admin (CODERUNNER_ADMIN_EMAIL)", () => { const afterSecond = JSON.parse( await readFile(allowlistPath, "utf8"), ) as { emails: string[] }; - expect(afterSecond.emails).toEqual(["boss@team.org", "coach@team.org"]); + expect(afterSecond.emails).toEqual([ + "boss@team.org", + "coach@team.org", + "lead@team.org", + ]); const coach = second.storage.db .query("SELECT role, updatedAt FROM user WHERE id = ?") @@ -357,6 +391,11 @@ describe("bootstrap admin (CODERUNNER_ADMIN_EMAIL)", () => { expect(boss.role).toBe("admin"); // The already-admin row is not rewritten. expect(boss.updatedAt).toBe(staleAdminTimestamp); + + const lead = second.storage.db + .query("SELECT role FROM user WHERE email = ?") + .get("lead@team.org") as { role: string }; + expect(lead.role).toBe("student"); } finally { second.close(); } @@ -381,10 +420,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 +474,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..efae98ca --- /dev/null +++ b/apps/control/src/__tests__/auth0.test.ts @@ -0,0 +1,323 @@ +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("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", () => { + 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); + }); + + 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 4d76245c..36997abd 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 */ @@ -17,10 +17,29 @@ import type { ControlConfig } from "../config"; import { getLogger } from "../logging"; import { isEmailAllowed, reloadAllowlist } from "./allowlist"; import { classroomPlugin } from "./classroom-plugin"; -import { buildSocialProviders } 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."; + +/** 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" + ); +} + /** * Reload allowlist.json from disk before enforcing it, so CLI edits * (`coderunner allowlist add`) take effect on the next sign-in attempt @@ -68,6 +87,7 @@ export function createAuth( callbacks: AuthCallbacks, ) { const socialProviders = config.demo ? {} : buildSocialProviders(config); + const auth0Plugin = config.demo ? null : buildAuth0Plugin(config); const options: BetterAuthOptions = { database: db, @@ -81,6 +101,7 @@ export function createAuth( ensureWorkspace: callbacks.ensureWorkspace, audit: callbacks.audit, }), + ...(auth0Plugin ? [auth0Plugin] : []), ], session: { expiresIn: 14 * 24 * 60 * 60, // 14 days in seconds @@ -127,7 +148,7 @@ export function createAuth( databaseHooks: { user: { create: { - before: async (user) => { + before: async (user, ctx) => { const classroomId = (user as { classroomId?: string | null }) .classroomId; if (classroomId) { @@ -136,6 +157,19 @@ export function createAuth( log.info("creating classroom guest", { classroomId, slug }); return { data: { ...user, slug, role: "student" } }; } + const slug = slugFromEmail(user.email); + if (isAuth0Callback(ctx)) { + // Auth0 vets the user; the role claim replaces the allowlist. + // 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, + role, + }); + return { data: { ...user, slug, role } }; + } // Enforce allowlist on new user creation await refreshAllowlistBeforeCheck(); if (!isEmailAllowed(user.email)) { @@ -143,11 +177,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"; @@ -165,13 +197,16 @@ export function createAuth( }, 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, }); @@ -180,8 +215,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..df641e9e 100644 --- a/apps/control/src/auth/providers.ts +++ b/apps/control/src/auth/providers.ts @@ -1,10 +1,19 @@ /** * 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 { 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?: { @@ -19,7 +28,7 @@ export type SocialProviders = { }; }; -export type OAuthProvider = "github" | "google"; +export type OAuthProvider = "github" | "google" | "auth0"; export function getEnabledAuthProviders( config: ControlConfig, @@ -34,6 +43,10 @@ export function getEnabledAuthProviders( providers.push("google"); } + if (config.auth0Domain && config.auth0ClientId && config.auth0ClientSecret) { + providers.push("auth0"); + } + return providers; } @@ -60,3 +73,72 @@ 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, + // 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); + 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; + }, + }, + ], + }); +} + +/** + * Map an Auth0 user's ID-token claims to a CodeRunner role. Admin emails + * (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 { + // 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]; + 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 00188ac2..8dec3292 100644 --- a/apps/control/src/storage.ts +++ b/apps/control/src/storage.ts @@ -184,15 +184,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, }); } } @@ -205,7 +211,8 @@ export class AppStorage { /** * Seed the accounts named in CODERUNNER_ADMIN_EMAIL so a fresh deployment * needs zero exec steps: each email is added to the allowlist (idempotent), - * and any existing non-admin account with that email is promoted. Accounts + * and any existing non-admin account with that email is promoted (except + * Auth0 users, whose role comes from Auth0 at sign-in). Accounts * that have not signed in yet get the admin role from the user.create hook. */ private async seedBootstrapAdmins(): Promise { @@ -215,8 +222,14 @@ export class AppStorage { // config.adminEmails is lowercased; the stored email keeps whatever // case the OAuth provider returned, so match case-insensitively. + // Rows with an Auth0 account are skipped: Auth0 sets the role at every + // sign-in and only makes an admin email admin once it is verified, + // while a row here may hold an unverified Auth0 self-sign-up (and any + // GitHub/Google identity later linked onto it). const user = this.db - .query("SELECT id, role FROM user WHERE lower(email) = ?") + .query( + "SELECT id, role FROM user WHERE lower(email) = ? AND NOT EXISTS (SELECT 1 FROM account WHERE account.userId = user.id AND account.providerId = 'auth0')", + ) .get(email) as { id: string; role: string | null } | null; if (user && user.role !== "admin") { this.db 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 32be02ce..869a01f5 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/deploy/cloud-init/user-data.yaml b/deploy/cloud-init/user-data.yaml index a5f8c961..7b91654c 100644 --- a/deploy/cloud-init/user-data.yaml +++ b/deploy/cloud-init/user-data.yaml @@ -64,6 +64,10 @@ write_files: GH_CLIENT_SECRET="$(fetch coderunner-github-client-secret)" GOOG_CLIENT_ID="$(fetch coderunner-google-client-id)" GOOG_CLIENT_SECRET="$(fetch coderunner-google-client-secret)" + # Optional: Auth0 is enabled only when all three are populated. + AUTH0_DOMAIN_VAL="$(fetch coderunner-auth0-domain)" + AUTH0_CLIENT_ID_VAL="$(fetch coderunner-auth0-client-id)" + AUTH0_CLIENT_SECRET_VAL="$(fetch coderunner-auth0-client-secret)" ADMIN_TOKEN_VAL="$(fetch coderunner-admin-token)" METRICS_TOKEN_VAL="$(fetch coderunner-metrics-token)" GC_URL="$(fetch coderunner-grafana-cloud-url)" @@ -123,6 +127,11 @@ write_files: echo "GITHUB_CLIENT_SECRET=$GH_CLIENT_SECRET" echo "GOOGLE_CLIENT_ID=$GOOG_CLIENT_ID" echo "GOOGLE_CLIENT_SECRET=$GOOG_CLIENT_SECRET" + if [ -n "$AUTH0_DOMAIN_VAL" ] && [ -n "$AUTH0_CLIENT_ID_VAL" ] && [ -n "$AUTH0_CLIENT_SECRET_VAL" ]; then + echo "AUTH0_DOMAIN=$AUTH0_DOMAIN_VAL" + echo "AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID_VAL" + echo "AUTH0_CLIENT_SECRET=$AUTH0_CLIENT_SECRET_VAL" + fi echo "ADMIN_TOKEN=$ADMIN_TOKEN_VAL" echo "METRICS_TOKEN=$METRICS_TOKEN_VAL" echo "CODE_MEMORY_LIMIT=4096m" diff --git a/deploy/terraform/secrets.tf b/deploy/terraform/secrets.tf index 9ed714fa..4857031a 100644 --- a/deploy/terraform/secrets.tf +++ b/deploy/terraform/secrets.tf @@ -9,6 +9,9 @@ locals { "coderunner-github-client-secret", "coderunner-google-client-id", "coderunner-google-client-secret", + "coderunner-auth0-domain", + "coderunner-auth0-client-id", + "coderunner-auth0-client-secret", "coderunner-admin-token", "coderunner-metrics-token", "coderunner-grafana-cloud-url", 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..8b25003f --- /dev/null +++ b/docs/decisions/044-auth0-sso.md @@ -0,0 +1,70 @@ +# 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 + — 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. For the + same reason the startup bootstrap promotion skips any user with an `auth0` + account: that row may be an unverified self-sign-up (Better Auth would even + link the real admin's later GitHub sign-in onto it), and Auth0 users get + their role at sign-in anyway. +- 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. +- 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 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 +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), 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/gcloud.md b/docs/deploying/gcloud.md index 957a8277..63e7db1c 100644 --- a/docs/deploying/gcloud.md +++ b/docs/deploying/gcloud.md @@ -128,6 +128,12 @@ echo -n '' | gcloud secrets versions add coderunner-g echo -n '' | gcloud secrets versions add coderunner-google-client-id --data-file=- echo -n '' | gcloud secrets versions add coderunner-google-client-secret --data-file=- +# Optional: Auth0 SSO. Skip these three to keep Auth0 off. +# Callback URL: https:///api/auth/oauth2/callback/auth0 +echo -n '.us.auth0.com' | gcloud secrets versions add coderunner-auth0-domain --data-file=- +echo -n '' | gcloud secrets versions add coderunner-auth0-client-id --data-file=- +echo -n '' | gcloud secrets versions add coderunner-auth0-client-secret --data-file=- + # Grafana Cloud (create a free stack at grafana.com) echo -n 'https://prometheus-prod-XX-prod-us-central-0.grafana.net/api/prom/push' \ | gcloud secrets versions add coderunner-grafana-cloud-url --data-file=- @@ -144,7 +150,8 @@ echo -n '' \ The exact secret names (all prefixed `coderunner-`) are defined in [`deploy/terraform/secrets.tf`](https://github.com/mathewdunne/CodeRunner/blob/main/deploy/terraform/secrets.tf). The `render-env.sh` script on the VM reads every one of them at boot; if any -are missing the service will not start. +are missing the service will not start. The `coderunner-auth0-*` secrets are the +exception: they are optional, and Auth0 is enabled only when all three are set. See [Grafana Cloud](../operating/grafana.md) for where to find each value in the Grafana Cloud portal. @@ -309,8 +316,8 @@ The VM's `.env` is regenerated on every boot from `render-env.sh`. Changes fall into three buckets: **A. Secrets already wired to Secret Manager** (`BETTER_AUTH_SECRET`, -`GITHUB_CLIENT_*`, `GOOGLE_CLIENT_*`, `ADMIN_TOKEN`, `METRICS_TOKEN`, Grafana -Cloud values): update the secret version, then re-render: +`GITHUB_CLIENT_*`, `GOOGLE_CLIENT_*`, `AUTH0_DOMAIN`, `AUTH0_CLIENT_*`, +`ADMIN_TOKEN`, `METRICS_TOKEN`, Grafana Cloud values): update the secret version, then re-render: ```bash printf '' | gcloud secrets versions add coderunner- --data-file=- diff --git a/docs/deploying/oauth-credentials.md b/docs/deploying/oauth-credentials.md index b526cccf..d87a0374 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,55 @@ 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` 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. + +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 +128,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 +146,10 @@ 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 if Auth0 has verified the +address. ### The easy path: `CODERUNNER_ADMIN_EMAIL` @@ -97,7 +159,9 @@ automatically — **no exec commands needed**. At startup the control plane adds each listed email to the allowlist, and on first OAuth sign-in the account is created with the admin role. An account that already exists with that email is promoted to admin at the next startup, so it also rescues a coach who signed in -before the env var was set. +before the env var was set. (Auth0 accounts are not promoted at startup; they +pick up the admin role at their next Auth0 sign-in once Auth0 has verified the +address.) ```bash CODERUNNER_ADMIN_EMAIL=coach@frcteam.org,assistant@frcteam.org 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/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index b95eedc4..c7e740e5 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 · ~321,074 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 4648 nodes · 6821 edges · 383 communities (293 shown, 90 thin omitted) +- 4676 nodes · 6860 edges · 382 communities (293 shown, 89 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: `b80814d2` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). @@ -312,7 +312,7 @@ - [[_COMMUNITY_Community 294|Community 294]] - [[_COMMUNITY_Community 295|Community 295]] - [[_COMMUNITY_Community 296|Community 296]] -- [[_COMMUNITY_Community 297|Community 297]] +- [[_COMMUNITY_Community 298|Community 298]] - [[_COMMUNITY_Community 299|Community 299]] - [[_COMMUNITY_Community 300|Community 300]] - [[_COMMUNITY_Community 301|Community 301]] @@ -323,12 +323,12 @@ - [[_COMMUNITY_Community 306|Community 306]] - [[_COMMUNITY_Community 307|Community 307]] - [[_COMMUNITY_Community 308|Community 308]] -- [[_COMMUNITY_Community 309|Community 309]] +- [[_COMMUNITY_Community 310|Community 310]] - [[_COMMUNITY_Community 311|Community 311]] - [[_COMMUNITY_Community 312|Community 312]] - [[_COMMUNITY_Community 313|Community 313]] - [[_COMMUNITY_Community 314|Community 314]] -- [[_COMMUNITY_Community 315|Community 315]] +- [[_COMMUNITY_Community 322|Community 322]] - [[_COMMUNITY_Community 323|Community 323]] - [[_COMMUNITY_Community 324|Community 324]] - [[_COMMUNITY_Community 325|Community 325]] @@ -383,22 +383,21 @@ - [[_COMMUNITY_Community 374|Community 374]] - [[_COMMUNITY_Community 375|Community 375]] - [[_COMMUNITY_Community 376|Community 376]] -- [[_COMMUNITY_Community 377|Community 377]] -- [[_COMMUNITY_Community 379|Community 379]] +- [[_COMMUNITY_Community 378|Community 378]] +- [[_COMMUNITY_Community 380|Community 380]] - [[_COMMUNITY_Community 381|Community 381]] -- [[_COMMUNITY_Community 382|Community 382]] ## God Nodes (most connected - your core abstractions) 1. `cn()` - 83 edges 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,32 +416,32 @@ - **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 (382 total, 89 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.06 +Nodes (37): BINDING_GROUPS, Bumper(), ControlsPanelProps, DpadArm(), FaceButton(), GamepadVisualizer(), SmallButton(), StatusTone (+29 more) ### Community 2 - "App Storage (SQLite)" -Cohesion: 0.05 -Nodes (41): sendUpstreamWebSocketMessage(), AppSocket, log, WebSocketHandlerContext, first, NUM_RUNS, second, cloneCall (+33 more) +Cohesion: 0.07 +Nodes (48): AdminClassroomContext, ClassroomJoinContext, classroomView(), clientIp(), handleAdminClassroomRoute(), handleClassroomJoin(), log, retirePreviousSession() (+40 more) ### Community 3 - "IDE Layout Resizable Panes" Cohesion: 0.08 -Nodes (36): AdminApp(), AdminLayout(), Tab, tabs, useAdminPoll(), Allowlist(), AllowlistData, AuditEntry (+28 more) +Nodes (33): AdminLayout(), Tab, tabs, useAdminPoll(), Allowlist(), AllowlistData, AuditEntry, AuditLog() (+25 more) ### Community 4 - "WebSocket Handler Factory" -Cohesion: 0.06 -Nodes (47): Args, backupDatabase(), dirExists(), fileExists(), main(), parseArgs(), runTar(), timestamp() (+39 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) +### Community 5 - "Control Plane App & Metrics" +Cohesion: 0.05 +Nodes (40): AllianceToggle(), AllianceToggleProps, Side, SIDE_ACTIVE, SIDE_EDGE, SIDE_STATION, sideOf(), onSelect (+32 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) @@ -452,160 +451,160 @@ 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 8 - "Shared UI Primitives" -Cohesion: 0.05 -Nodes (25): ResolvedTheme, Theme, THEME_VALUES, ThemeProvider(), ThemeProviderContext, ThemeProviderProps, ThemeProviderState, useTheme() (+17 more) - -### Community 9 - "Docker Runtime Provider Core" Cohesion: 0.07 Nodes (24): chooserRoots(), decodeMsgPack(), displayKey(), encodeMsgPack(), isChooser(), normalizeTopicName(), Nt4AutoChooserBridge, Nt4AutoChooserBridgeOptions (+16 more) -### Community 10 - "Run Execution Metrics" -Cohesion: 0.06 -Nodes (35): ADMIN_GET_ROUTES, adminCookie, cookie, req, studentCookie, ManagedWorkspaceRuntime, WorkspaceRuntimeCommand, adminCookie() (+27 more) +### Community 9 - "Docker Runtime Provider Core" +Cohesion: 0.05 +Nodes (40): createClassroom(), attacker, classroom, coachRow, firstBody, firstCookie, firstSessions, firstWorkspace (+32 more) -### Community 11 - "Driver Station Enable/Disable UI" +### Community 10 - "Run Execution Metrics" Cohesion: 0.05 Nodes (45): listWorkspaceDiskLimitDevices, parseDockerStatsLine, runtimeFromLease, statusFromLease, upstreamEndpoints, dockerPortBindError, inspectContainer, inspectContainers (+37 more) +### Community 11 - "Driver Station Enable/Disable UI" +Cohesion: 0.06 +Nodes (34): activeWorkspaces, DockerStatsPoller, DockerStatsPollerOptions, log, containerCpuPercent, containerMemoryPercent, containerStartBuckets, containerStartDuration (+26 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.11 +Nodes (33): AdminRouteContext, log, createProjectArchive(), directorySizeBytes(), restoreProjectArchive(), runTar(), parseDeployFilePath(), nt4AliveResponse() (+25 more) ### Community 13 - "Control Plane Route Test Suite" Cohesion: 0.06 -Nodes (34): activeWorkspaces, DockerStatsPoller, DockerStatsPollerOptions, log, containerCpuPercent, containerMemoryPercent, containerStartBuckets, containerStartDuration (+26 more) +Nodes (23): AdminApp(), ResolvedTheme, Theme, THEME_VALUES, ThemeProvider(), ThemeProviderContext, ThemeProviderProps, ThemeProviderState (+15 more) ### Community 14 - "NT4 Auto Chooser Protocol" -Cohesion: 0.11 -Nodes (23): body, body, body, body, absolute, refs, headers, received (+15 more) +Cohesion: 0.1 +Nodes (4): statusFromLease(), LocalDockerRuntimeProvider, containerRuntimeState(), release ### Community 15 - "E2E Runtime & Resilience Specs" -Cohesion: 0.11 -Nodes (31): parseDockerStatsLine(), parsePercent(), defaultDockerRunner(), dockerError(), inspectContainer(), inspectContainerOrThrow(), inspectContainers(), runDocker() (+23 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 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.08 +Nodes (30): halsimWebSocketResponse(), HOP_BY_HOP_HEADERS, log, nt4WebSocketResponse(), probeVscodeReady(), requestedProtocols(), sendUpstreamWebSocketMessage(), stripHopByHopHeaders() (+22 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 (32): ADMIN_GET_ROUTES, adminCookie, cookie, req, studentCookie, AuditLogEntry, adminCookie, body (+24 more) ### Community 18 - "Auth & Allowlist Test Bodies" -Cohesion: 0.09 -Nodes (10): setAllowlistPath(), AppStorage, ensureWorkspaceFiles(), log, nowIso(), projectPathFor(), randomId(), RunJobRow (+2 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 19 - "WebSocket Upstream Proxy" -Cohesion: 0.07 -Nodes (35): AutoPanel, preferredChooser, CodeStatusPill, codeStatusFromRun, ConsolePanel, ConsoleViewport, ControlsPanel, GamepadVisualizer (+27 more) +Cohesion: 0.11 +Nodes (28): runtimeFromLease(), upstreamEndpoints(), CapacityExceededError, log, configMountType(), containerAttachedToNetwork(), containerHasPublishedPorts(), isLoopbackHost() (+20 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.13 +Nodes (20): body, body, body, body, absolute, refs, headers, received (+12 more) ### Community 21 - "Import URL Security Tests" -Cohesion: 0.06 -Nodes (30): AuditLogEntry, adminCookie, body, fakeDocker, rows, student, studentCookie, workspace (+22 more) +Cohesion: 0.09 +Nodes (11): ControlConfigInput, AppStorage, createStorage(), ensureWorkspaceFiles(), log, nowIso(), projectPathFor(), randomId() (+3 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) +Nodes (21): PreviewFrame, PreviewPane(), PreviewPaneProps, PreviewPlaceholderProps, Doc, fetchMock, firstKey, GUIDE (+13 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) - -### 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 25 - "Admin Backup/Restore Test Suite" +Cohesion: 0.12 +Nodes (27): handleAdminRoute(), AssetManifest, contentTypeFor(), handleUploadAsset(), HAS_PROC_SELF_FD, isInsideDirectory(), pathplannerResponse(), readScopeAssetManifest() (+19 more) + ### Community 26 - "Better Auth Providers & Storage" -Cohesion: 0.08 -Nodes (21): PreviewFrame, PreviewPane(), PreviewPaneProps, PreviewPlaceholderProps, Doc, fetchMock, firstKey, GUIDE (+13 more) +Cohesion: 0.07 +Nodes (31): audit_log table, runtime_config table, audit log test suite, recordAuditEvent (audit.ts, tested), Physical disk vs virtual device filtering for --device-read-bps, listWorkspaceDiskLimitDevices (containers/block-devices), container concurrency cap test suite, max-active-containers cap persisted via runtime_config, adoption/restart gating (+23 more) ### Community 27 - "AdvantageScope Patch Application" -Cohesion: 0.12 -Nodes (26): AdminRouteContext, handleAdminRoute(), log, createProjectArchive(), directorySizeBytes(), restoreProjectArchive(), runTar(), apiErrorResponse() (+18 more) +Cohesion: 0.09 +Nodes (19): AutoPanel(), AutoPanelProps, ControlsPanel(), DriverStation(), DriverStationProps, change, { container, rerender }, props (+11 more) ### Community 28 - "Allowlist Management E2E" -Cohesion: 0.06 -Nodes (28): bashCalls, cloneCall, ctx, importer, mock, row, workspace, GithubImportContext (+20 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 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.09 +Nodes (10): AppFixtures, seedRuntimeRunning(), seedWorkspaceProject(), WorkspacePage, status, wsp, Deps, dialog (+2 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 (16): classroom, guests, leave, switchProject, AppOptions, defaultRunCommandFactory, startFakeHalsim(), startFakeVscode() (+8 more) ### Community 31 - "Sim API & Chooser Announcements" -Cohesion: 0.07 -Nodes (26): audit, body, classroom, cleared, cookie, ended, expired, [guest] (+18 more) +Cohesion: 0.1 +Nodes (21): SimPanePanels(), SimPanePanelsProps, SimPaneTab, SimPaneTabs(), SimPaneTabSelector(), SimPaneTabsProps, onPreviewActivated, pathplannerTab (+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) +Cohesion: 0.09 +Nodes (28): BACKEND_ORIGIN Pages secret, deploy-cloudflare job in deploy.yml, Cloudflare Offline Page, Pages Function catch-all [[path]].ts, deploy/cloudflare/wrangler.toml, coderunner-ops.json dashboard, Deployment Overview, CodeRunner Docs Overview (+20 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.12 +Nodes (19): LayoutMenu(), Avatar(), formatEndsAt(), leaveClassroom(), signOut(), UserMenu(), UserMenuProps, DropdownMenu() (+11 more) ### Community 34 - "Control Plane Config Loading" -Cohesion: 0.1 -Nodes (21): SimPanePanels(), SimPanePanelsProps, SimPaneTab, SimPaneTabs(), SimPaneTabSelector(), SimPaneTabsProps, onPreviewActivated, pathplannerTab (+13 more) +Cohesion: 0.11 +Nodes (23): reloadAllowlist(), Auth, AuthCallbacks, createAuth(), log, refreshAllowlistBeforeCheck(), slugFromEmail(), slugify() (+15 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.07 +Nodes (21): audit, before, { classroom }, { classrooms }, ClassroomView, deletes, end, expired (+13 more) ### Community 36 - "Structured Logging" Cohesion: 0.07 -Nodes (22): audit, before, { classroom }, { classrooms }, ClassroomView, deletes, end, expired (+14 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 37 - "Admin App Allowlist Page" -Cohesion: 0.11 -Nodes (17): CATALOG, fetchMock, { result }, useLessons(), UseLessonsReturn, INITIAL_STATE, ProjectSwapKind, ProjectSwapState (+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 38 - "Allowlist Core Module" -Cohesion: 0.12 -Nodes (19): LayoutMenu(), Avatar(), formatEndsAt(), leaveClassroom(), signOut(), UserMenu(), UserMenuProps, DropdownMenu() (+11 more) +Cohesion: 0.11 +Nodes (14): BundledCatalogSource, CatalogManifest, CatalogSource, findModuleOrThrow(), log, parseCatalogRepo(), RemoteCatalogSource, sortByOrder() (+6 more) ### 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.16 +Nodes (5): consumeLines(), lineLooksReady(), randomRunId(), runLogPath(), RunManager ### 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.08 +Nodes (25): addBody, adminCookie, after, aliceWorkspace, backedUpProject, backupsDir, bob, bobWorkspace (+17 more) ### Community 41 - "Audit Log & Runtime Config Schema" Cohesion: 0.11 -Nodes (15): DemoBanner(), EditorPane(), EditorPaneProps, ScopePane, EditorReachability, EditorStatus, { result }, useEditorReachability() (+7 more) +Nodes (19): join, baBody, body, userRow, workspace, denied, GATED_PATHS, PUBLIC_PATHS (+11 more) ### Community 42 - "E2E Workspace Fixtures" -Cohesion: 0.16 -Nodes (5): consumeLines(), lineLooksReady(), randomRunId(), runLogPath(), RunManager - -### Community 43 - "Gamepad Input Mapping" Cohesion: 0.08 Nodes (24): aliceWorkspace, bobWorkspace, byName, calls, config, cookie, expectedName, fakeDocker (+16 more) +### Community 43 - "Gamepad Input Mapping" +Cohesion: 0.15 +Nodes (22): isOpenFileInsideRoot(), ASSET_CONTENT_TYPES, discoverDocuments(), documentCsp(), documentKindFor(), errorDocument(), log, documentShell() (+14 more) + ### Community 44 - "PathPlanner Integration Deploy Pipeline" Cohesion: 0.08 -Nodes (25): addBody, adminCookie, after, aliceWorkspace, backedUpProject, backupsDir, bob, bobWorkspace (+17 more) +Nodes (21): RateLimitError, bobCookie, body, clearedCache, cloneCall, cookie, copyCall, ctx (+13 more) ### Community 45 - "Gamepad Lease Management" Cohesion: 0.14 Nodes (23): applyAdvantageScopePatches(), ascopeRoot, CommandResult, patchDir, patchFiles(), repoRoot, run(), ascopeLiteStatic (+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) +Cohesion: 0.09 +Nodes (25): container_leases table (V1 core schema), run_jobs table (V1 core schema), sessions table (V1 core schema), users table (V1 core schema), workspaces table (V1 core schema), idx_container_leases_lsp_port_unique, idx_container_leases_sim_port_unique, container_leases.lsp_state column (+17 more) ### Community 47 - "Run Lifecycle Test Fixtures" Cohesion: 0.11 @@ -620,8 +619,8 @@ Cohesion: 0.08 Nodes (22): aliceConnection, aliceMessages, aliceRun, aliceRunId, aliceWorkspace, baseOptions, bobConnection, bobMessages (+14 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.08 +Nodes (23): afterFirst, afterSecond, aliceCookie, allowlistPath, baseOptions, boss, coach, cookie (+15 more) ### Community 51 - "Gamepad/Driver Station Zod Schemas" Cohesion: 0.08 @@ -636,36 +635,36 @@ 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) ### 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.09 +Nodes (20): AdvantageScope Lite NT4 client, test (Playwright base.extend AppFixtures), apps/control/src/app.ts, e2e/fixtures/app.ts (ControlApp test fixture), apps/control/src/app/assets.ts, apps/control/src/config.ts (ControlConfig), createApp() (ControlApp factory), createPathPlannerDist() (+12 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.13 +Nodes (22): ANSI, colorize(), configureLogging(), createJsonSink(), createSink(), formatAttrs(), formatAttrValue(), formatRecord() (+14 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.11 +Nodes (23): AdminApp, AdminLayout, Tab (type), Allowlist (admin page), fetchAllowlist, App (default export), FallbackRedirect, RootIndex (+15 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.15 +Nodes (19): defaultContainerUser(), defaultDataDir, defaultHalsimPortRange, defaultSimPortRange, defaultVscodePortRange, DISABLED_DISK_LIMIT_VALUES, envContainerUser(), loadControlConfig() (+11 more) ### Community 58 - "Asset Upload & Manifest" -Cohesion: 0.13 -Nodes (12): AppOptions, defaultRunCommandFactory, startFakeHalsim(), startFakeVscode(), AppFixture, FakeHalsimHandle, FakeVscodeHandle, SeededUser (+4 more) +Cohesion: 0.14 +Nodes (18): DEFAULT_NETWORKS, deriveComposeProject(), deriveContainerUser(), deriveHostDataDir(), deriveNetwork(), inspectFailureMessage(), notDerived(), readComposeProject() (+10 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.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 60 - "Run Manager Command Factory" -Cohesion: 0.1 -Nodes (18): aliceCookie, bobCookie, byPath, cookie, docker, encoded, listed, names (+10 more) +Cohesion: 0.14 +Nodes (14): GamepadFrame, GamepadInfo, listConnectedGamepads(), makeLabel(), FakePad, { result }, useGamepad(), UseGamepadResult (+6 more) ### Community 61 - "Import Pipeline Test Fixtures" Cohesion: 0.1 -Nodes (20): afterFirst, afterSecond, aliceCookie, allowlistPath, baseOptions, boss, coach, cookie (+12 more) +Nodes (18): aliceCookie, bobCookie, byPath, cookie, docker, encoded, listed, names (+10 more) ### Community 62 - "Path Planner Pane.test" Cohesion: 0.1 @@ -676,720 +675,724 @@ 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 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) +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 65 - "Admin Allowlist Endpoints" +Cohesion: 0.1 +Nodes (14): cookie, snapshot, cookie, firstBody, rows, runtime2, secondBody, states (+6 more) + +### Community 66 - "AdvantageScope Lite & Docusaurus" Cohesion: 0.11 Nodes (16): DEFAULT_PORT, GamepadLease, GamepadLeaseResolver, GamepadMessageOutcome, GamepadStatus, log, SessionState, HalSimBridgeUnavailableError (+8 more) -### Community 66 - "AdvantageScope Lite & Docusaurus" +### Community 67 - "Workspace Container Bind-Mount & Memory Bounds" +Cohesion: 0.1 +Nodes (18): workspaceBySlug(), config, cookie, dataDir, fakeDocker, lease, name, now (+10 more) + +### Community 68 - "Slug/Login Property Tests" Cohesion: 0.15 Nodes (15): clearContainerLeases(), dbPathFromEnv(), DockerCommandResult, DockerRunner, main(), parseContainerNames(), rebuildWorkspaces(), RebuildWorkspacesOptions (+7 more) -### Community 67 - "Workspace Container Bind-Mount & Memory Bounds" +### Community 69 - "Scripts" 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) -### Community 68 - "Slug/Login Property Tests" +### Community 70 - "Mock Workspace Runtime Provider" 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 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) - -### Community 70 - "Mock Workspace Runtime Provider" +### Community 71 - "WebSocket Router Test Suite" Cohesion: 0.16 Nodes (16): digitalAxis(), dpadToPov(), gamepadStateToVisualizerFrame(), isMappedKeyboardCode(), KEYBOARD_BINDINGS, KeyboardBindingGroup, keyboardCodesToWpilib(), MAPPED_CODES (+8 more) -### Community 71 - "WebSocket Router Test Suite" +### Community 72 - "Deploy Files Path Safety" +Cohesion: 0.13 +Nodes (9): RunConnection, RunStatus, FakeSocket, Listener, { rerender }, { result }, { unmount }, useRunChannel() (+1 more) + +### Community 73 - "Java Tooling & Test Tiers" +Cohesion: 0.11 +Nodes (13): CATALOG, fetchMock, { result }, useLessons(), UseLessonsReturn, INITIAL_STATE, ProjectSwapKind, ProjectSwapState (+5 more) + +### Community 74 - "VSCodium Editor Container Defaults" +Cohesion: 0.13 +Nodes (15): first, NUM_RUNS, second, cases, expectRejected(), result, CatalogLoadContext, ImportContext (+7 more) + +### Community 75 - "Contracts Schema Unit Tests" +Cohesion: 0.22 +Nodes (14): addAllowlistEntry(), AllowlistData, EMPTY, getAllowlist(), isEmailAllowed(), loadAllowlist(), normalize(), normalizeEntry() (+6 more) + +### Community 76 - "Import Manager (Git Clone Staging)" 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 77 - "Demo Mode Session Seeding" 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 73 - "Java Tooling & Test Tiers" +### Community 78 - "Backup/Restore & Catalog Import Flows" +Cohesion: 0.14 +Nodes (19): handleAdminRoute, directorySizeBytes, isInsideDirectory, pathplannerResponse, readScopeAssetManifest, safeRelativeAssetPath, scopeResponse, staticFileResponse (+11 more) + +### Community 79 - "Auth Client & Docker Runner Helpers" 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) -### Community 74 - "VSCodium Editor Container Defaults" +### Community 80 - "HALSim Test Suite" +Cohesion: 0.12 +Nodes (19): 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 (Control-Plane Unit/Integration Tests), bun run test:web (Frontend Unit/Component Tests) (+11 more) + +### Community 81 - "Bundled Lesson Catalog Modules" Cohesion: 0.21 Nodes (3): defaultSnapshot(), HalSimBridge, upstreamUrlFor() -### Community 75 - "Contracts Schema Unit Tests" +### Community 82 - "Fake NT4 Test Server" Cohesion: 0.11 Nodes (17): bobCookie, body, cookie, docker, evilDir, evilPath, example, newFile (+9 more) -### Community 76 - "Import Manager (Git Clone Staging)" +### Community 83 - "HALSim Bridge Message Handling" 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 77 - "Demo Mode Session Seeding" +### Community 84 - "Database Migrations Runner" 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) -### Community 78 - "Backup/Restore & Catalog Import Flows" +### Community 85 - "Asset Upload Test Suite" 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) -### Community 79 - "Auth Client & Docker Runner Helpers" +### Community 86 - "Capacity Cap Test Suite" 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) -### 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) - -### 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) - -### Community 82 - "Fake NT4 Test Server" +### Community 87 - "Disk Read Limit Config" 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) - -### 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) +Nodes (18): BundledCatalogSource (catalog.ts), CatalogSource interface, createCatalogSource (catalog.ts), parseCatalogRepo (catalog.ts), RemoteCatalogSource (catalog.ts), Two-source lesson catalog pattern (bundled + remote), ImportError (imports.ts), sendUpstreamWebSocketMessage (+10 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) +### Community 88 - "" +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 85 - "Asset Upload Test Suite" +### Community 89 - "AdvantageScope Lite Hosting (Archived)" Cohesion: 0.18 Nodes (15): docker(), DockerResult, migratedSettings, openJavaFile(), repoRoot, startWorkspace(), stopWorkspace(), terminal (+7 more) -### Community 86 - "Capacity Cap Test Suite" +### Community 90 - "Driver Station Page Object & Runtime Seeding" Cohesion: 0.12 Nodes (16): first, NUM_RUNS, parsed, round, slugArb, allianceStationSchema, bridgeConnectionSchema, containerStateSchema (+8 more) -### Community 87 - "Disk Read Limit Config" -Cohesion: 0.21 -Nodes (16): isInsideDirectory(), isOpenFileInsideRoot(), collectFiles(), deployFileDeleteResponse(), deployFilesSnapshotResponse(), deployFileWriteResponse(), DeployWorkspace, findDeepestExistingAncestor() (+8 more) +### Community 91 - "Rebuild Workspaces" +Cohesion: 0.19 +Nodes (15): parseDockerStatsLine(), parsePercent(), cleanupStoppedContainers(), countRunningContainers(), log, managedContainerStats(), removeCodeContainer(), removeCodeVolume() (+7 more) -### Community 88 - "" +### Community 92 - "Canonical Image Naming Decisions" 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 89 - "AdvantageScope Lite Hosting (Archived)" +### Community 93 - "Audit Log & Break-Glass Admin" 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) -### Community 90 - "Driver Station Page Object & Runtime Seeding" +### Community 94 - "Container Runtime Status Helpers" 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 91 - "Rebuild Workspaces" +### Community 95 - "Network Mode Config Test" 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) -### 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) +### Community 96 - "Simulation State Hook" +Cohesion: 0.14 +Nodes (17): Admin allowlist endpoints, addAllowlistEntry (auth/allowlist), isEmailAllowed (auth/allowlist), loadAllowlist (auth/allowlist), reloadAllowlist, removeAllowlistEntry, saveAllowlist, Reload allowlist before each sign-in check (+9 more) -### Community 93 - "Audit Log & Break-Glass Admin" +### Community 97 - "Rebuild Workspaces CLI Args" +Cohesion: 0.17 +Nodes (17): 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 (+9 more) + +### Community 98 - "Users CLI Script" 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) +### Community 99 - "Extension Reconciliation Test" +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 95 - "Network Mode Config Test" +### Community 100 - "Sim Api.test" +Cohesion: 0.17 +Nodes (9): DemoBanner(), ScopePane, useGamepadChannel(), ScopeStatus, useScopeHandshake(), WorkspacePage(), persisted, store (+1 more) + +### Community 102 - "README" Cohesion: 0.12 -Nodes (11): cookie, snapshot, cookie, firstBody, rows, runtime2, secondBody, states (+3 more) +Nodes (8): auth0Options, config, custom, nextIdTokenClaims, sessionsBefore, url, user, workspace -### Community 97 - "Rebuild Workspaces CLI Args" +### Community 103 - "Container Isolation & HALSim Decisions" +Cohesion: 0.13 +Nodes (13): defaultLogFormat(), parseLogFormatEnv(), banner, configInput, demoFlag, log, port, server (+5 more) + +### Community 104 - "Cloudflare Pages Proxy Function" Cohesion: 0.14 Nodes (13): BridgeEntry, DEFAULT_DRIVER_STATION, DriverStationState, HalSimBridgeOptions, HalSimBridgeSnapshot, HalSimMessage, HalSimWebSocketFactory, JoystickWireState (+5 more) -### Community 98 - "Users CLI Script" +### Community 105 - "Project Swap Fake Socket Test" 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 106 - "Proxy Header Test Suite" 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 107 - "Container Lease Schema (V1/V2)" 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" -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) +### Community 108 - "Core Schema Tables & Better Auth Migration" +Cohesion: 0.12 +Nodes (16): 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), PathPlanner subtree writable, choreo subtree read-only design, safeRelativeAssetPath (asset path validation) (+8 more) -### Community 102 - "README" +### Community 109 - "WebSocket Upstream Message Send" 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) +Nodes (16): applySecurityHeaders, authorizeMetrics, constantTimeEqual, dispatch, fetch (request handler), handleUploadAsset, userAssetsPath, webShellResponse (+8 more) -### Community 103 - "Container Isolation & HALSim Decisions" +### Community 110 - "Deployment Hardware & FAQ Docs" +Cohesion: 0.17 +Nodes (16): 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, scripts/image.ts, containers/code/root/.../init-frc-setup/run (+8 more) + +### Community 111 - "E2E ControlApp Test Fixture Setup" 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 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) +### Community 112 - "AS Lite NT4 Endpoint Injection Patch" +Cohesion: 0.23 +Nodes (14): collectFiles(), deployFileDeleteResponse(), deployFilesSnapshotResponse(), deployFileWriteResponse(), DeployWorkspace, findDeepestExistingAncestor(), HAS_PROC_SELF_FD, INVALID_PATH_ERROR (+6 more) -### Community 105 - "Project Swap Fake Socket Test" +### Community 113 - "Editor Pane Reachability" Cohesion: 0.13 Nodes (14): authProvidersResponseSchema, autoChooserPatchSchema, autoChoosersResponseSchema, deployFilePathSchema, deployFilesSnapshotResponseSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, importResponseSchema (+6 more) -### Community 106 - "Proxy Header Test Suite" -Cohesion: 0.16 -Nodes (6): FakeSocket, Listener, { rerender }, { result }, { unmount }, useRunChannel() - -### Community 108 - "Core Schema Tables & Better Auth Migration" -Cohesion: 0.13 -Nodes (13): SocketData, waitFor(), CloseCall, fakeDocker, FakeSocket, fakeUpstream, forwarded, hello (+5 more) +### Community 115 - "Driver Station Switch Project Flow" +Cohesion: 0.2 +Nodes (14): createWebSocketHandlers(), ascopeRoot, assert(), createCatalogDir(), distDir, exists(), patchDir, repoRoot (+6 more) -### Community 109 - "WebSocket Upstream Message Send" +### Community 116 - "Admin Workspace Backup/Restore Actions" 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 110 - "Deployment Hardware & FAQ Docs" +### Community 117 - "Auto Choosers & Container Status Hooks" 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) -### Community 111 - "E2E ControlApp Test Fixture Setup" +### Community 118 - "Java Tooling Compatibility Decisions" 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 112 - "AS Lite NT4 Endpoint Injection Patch" +### Community 119 - "Lessons & Testing Decision Docs" 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 113 - "Editor Pane Reachability" +### Community 120 - "Editor Migration Decisions" Cohesion: 0.13 Nodes (15): authClient (better-auth), defaultDockerRunner, inspectContainerOrThrow, configureLogging, getLogger, loadProviders, LoginPage Component, OAuthButton Component (+7 more) -### 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) - -### 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) - -### Community 116 - "Admin Workspace Backup/Restore Actions" +### Community 121 - "CLI Reference & Control Container" Cohesion: 0.14 Nodes (13): compact, console_, disable, editor, editorBody, editorControl, freshConsole, hideEditor (+5 more) -### Community 117 - "Auto Choosers & Container Status Hooks" +### Community 122 - "Gamepad Shim E2E Test" Cohesion: 0.2 -Nodes (7): IDELayout(), IDELayoutProps, FakeResizeObserver, useSplit(), ResizableHandle(), ResizablePanel(), ResizablePanelGroup() +Nodes (12): stripHopByHopHeaders, getDemoSession(), getDemoSessionResponseBody(), seedDemoUser(), isAllowedWebSocketOrigin(), isLoopbackHost(), log, normalizeHost() (+4 more) -### Community 118 - "Java Tooling Compatibility Decisions" +### Community 123 - "Catalog Integrity Test" Cohesion: 0.14 -Nodes (12): attacker, classroom, coachRow, firstBody, firstCookie, firstSessions, firstWorkspace, school (+4 more) +Nodes (12): waitFor(), CloseCall, fakeDocker, FakeSocket, fakeUpstream, forwarded, hello, importWs (+4 more) -### Community 119 - "Lessons & Testing Decision Docs" +### Community 124 - "Driver Station Page Object" Cohesion: 0.25 Nodes (10): DistDownload, downloadAndExtract(), run(), withScratch(), artifacts, main(), repoRoot, tagArgIndex (+2 more) -### Community 120 - "Editor Migration Decisions" +### Community 125 - "Fake Socket Test Double" 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) -### Community 121 - "CLI Reference & Control Container" +### Community 126 - "Session Hook & Heartbeat" 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 122 - "Gamepad Shim E2E Test" +### Community 127 - "Auto Choosers Hook 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) -### Community 123 - "Catalog Integrity Test" +### Community 128 - "Command Injection Test Suite" +Cohesion: 0.14 +Nodes (13): DriverStationPage (page object), e2e/fixtures/gamepad-shim.ts, installGamepadShim(), MockWorkspaceRuntimeProvider, Quick Start (Installation), e2e/fixtures/runtime.ts, e2e/fixtures/runtime.ts, seedRuntimeRunning() (+5 more) + +### Community 129 - "Default-Deny Route Coverage" 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) -### 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) - -### Community 126 - "Session Hook & Heartbeat" +### Community 130 - "Driver Station Page Object & Runtime Seeding" Cohesion: 0.17 Nodes (11): FakeNt4Handle, FakeNt4Options, startFakeNt4(), aliceMessages, aliceReady, aliceSocket, allAlice, allBob (+3 more) -### Community 127 - "Auto Choosers Hook Test" +### Community 131 - "Demo Mode Disk/Memory Limits" Cohesion: 0.21 Nodes (9): CodeStatus, CodeStatusPill(), TONES, codeStatusFromRun(), ConsoleLine(), ConsolePanel(), ConsolePanelProps, parseConsoleLine() (+1 more) -### Community 128 - "Command Injection Test Suite" +### Community 132 - "Img Screenshots" +Cohesion: 0.28 +Nodes (11): config, db, log, AppliedMigrationRow, applyMigrations(), ensureMigrationTable(), listAppliedMigrations(), loadMigrations() (+3 more) + +### Community 133 - "Build Failure.spec" Cohesion: 0.15 Nodes (12): assetDir, assetsDir, config, cookie, createAssetZip(), formData, manifest, proc (+4 more) -### Community 129 - "Default-Deny Route Coverage" +### Community 134 - "Admin.po" Cohesion: 0.17 Nodes (12): adminCookie, alice, aliceCookie, bob, bobCookie, body, carol, fakeContainerFor() (+4 more) -### Community 130 - "Driver Station Page Object & Runtime Seeding" +### Community 135 - "Java Tooling Smoke Test" 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 131 - "Demo Mode Disk/Memory Limits" +### Community 136 - "Ws Bridge" 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 132 - "Img Screenshots" +### Community 137 - "Main" 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) -### Community 133 - "Build Failure.spec" +### Community 138 - "Logging.test" 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) -### Community 134 - "Admin.po" +### Community 139 - "Metadata" +Cohesion: 0.21 +Nodes (13): AdminPage (page object), CLI Reference Doc, coderunner dispatching CLI, containers/control/entrypoint.sh, Configuration Reference, containers/control/Dockerfile, scripts/fetch-dist.ts, Troubleshooting Guide (+5 more) + +### Community 140 - "Middleware" +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 141 - "Image" Cohesion: 0.18 Nodes (8): SimRunAction, patchCall, postCall, { result }, updatedStatus, VALID_STATUS, useSimulationState(), UseSimulationStateReturn -### Community 135 - "Java Tooling Smoke Test" -Cohesion: 0.26 -Nodes (11): ascopeRoot, assert(), createCatalogDir(), distDir, exists(), patchDir, repoRoot, runGit() (+3 more) +### Community 142 - "023 Metrics And Observability" +Cohesion: 0.18 +Nodes (10): ExtensionRecord, fixture(), manifest(), old, reconcileScript, record(), repoRoot, staleDirectory (+2 more) -### Community 136 - "Ws Bridge" +### Community 143 - "User Data" Cohesion: 0.17 Nodes (9): args, argsPath, home, projectRoot, robotJar, sleeper, start, stop (+1 more) -### Community 137 - "Main" +### Community 144 - "Ws Proxy.spec" Cohesion: 0.32 Nodes (11): Args, dirExists(), discoverWorkspaces(), fileExists(), main(), parseArgs(), restoreArchive(), restoreDb() (+3 more) -### Community 138 - "Logging.test" +### Community 145 - "024 Container Memory Budget" 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) -### Community 139 - "Metadata" +### Community 146 - "Halsim.test" 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) -### Community 140 - "Middleware" +### Community 147 - "Gamepad.test" Cohesion: 0.17 Nodes (11): Architecture, code:text (Browser (one student)), code:text (data/), How a Run works, How PathPlanner files flow, How Preview reads documents, How telemetry flows, Persistence and data layout (+3 more) -### Community 141 - "Image" +### Community 148 - "Audit Prune" Cohesion: 0.17 Nodes (11): Changes to this policy, Children's privacy, Contact, How long it is kept, How the information is used, Privacy Policy, What information is collected, What is not done with it (+3 more) -### Community 142 - "023 Metrics And Observability" +### Community 149 - "Users" 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() +### Community 150 - "Helpers" +Cohesion: 0.17 +Nodes (12): createApp (app.ts), containers.ts barrel module, Control plane restart rediscovers labeled containers (reconciliation), seedDemoUser, createAdvantageScopeDist, createCatalogDir, createPathPlannerDist, createWebDist (+4 more) -### Community 144 - "Ws Proxy.spec" +### Community 151 - "Card" +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 152 - "Catalog.test" 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" +### Community 153 - "Block Devices" 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) -### Community 146 - "Halsim.test" +### Community 154 - "Ws Bridge" +Cohesion: 0.24 +Nodes (8): Env, isProxiedPath(), onRequest(), PagesFunctionContext, serviceUnavailable(), TOP_LEVEL_PROXIED, assetRequests, proxiedRequests + +### Community 155 - "Dropdown Menu" Cohesion: 0.25 Nodes (3): FakeSocket, Listener, { result } -### Community 147 - "Gamepad.test" +### Community 156 - "020 Workspace Runtime Provider" 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 157 - "030 Control Plane Hardening Pass" +Cohesion: 0.22 +Nodes (5): IDELayout(), IDELayoutProps, FakeResizeObserver, useSplit(), ResizablePanel() -### Community 149 - "Users" +### Community 158 - "Auth" +Cohesion: 0.2 +Nodes (8): missing(), aliceCookie, dockerRunner(), fakeDocker, receivedHeaders, result, source, startedAt + +### Community 159 - "2026 08 30 Pathplanner Integration" 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) -### Community 150 - "Helpers" +### Community 160 - "Xss.spec" 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) -### Community 151 - "Card" +### Community 161 - "Build Ascope Lite" 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) -### Community 152 - "Catalog.test" +### Community 162 - "Index" 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) -### Community 153 - "Block Devices" +### Community 163 - "Allowlist Management E2E" +Cohesion: 0.18 +Nodes (11): 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), 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 (+3 more) + +### Community 164 - "Use Container Status.test" 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) -### Community 154 - "Ws Bridge" +### Community 165 - "Frc Robot" 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) -### Community 155 - "Dropdown Menu" +### Community 166 - "Code Container Defaults.test" 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) -### Community 156 - "020 Workspace Runtime Provider" +### Community 167 - "Java LSP Bridge (Archived)" 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 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) - -### 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) - -### 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" +### Community 168 - "2026 08 30 Pathplanner Integration" 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) -### 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 165 - "Frc Robot" +### Community 169 - "Restore" +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 170 - "Login.po" Cohesion: 0.22 -Nodes (8): SimButton(), SimButtonProps, SimControlsBlock(), SimControlsBlockProps, onRestart, onStart, onStop, TONE_CLASSES +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 166 - "Code Container Defaults.test" +### Community 172 - "Auth Demo.test" Cohesion: 0.24 -Nodes (8): AllianceToggle(), AllianceToggleProps, Side, SIDE_ACTIVE, SIDE_EDGE, SIDE_STATION, sideOf(), onSelect +Nodes (6): EditorPane(), EditorPaneProps, EditorReachability, EditorStatus, { result }, useEditorReachability() -### Community 167 - "Java LSP Bridge (Archived)" +### Community 173 - "Robot" Cohesion: 0.31 Nodes (3): GamepadSessions, halsimTarget(), resolveLease() -### Community 168 - "2026 08 30 Pathplanner Integration" +### Community 174 - "Cleanup Containers" 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 175 - "Config" 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 176 - "Use Gamepad" 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 177 - "016 Project Import Strategy" 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 178 - "[[path]]" 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 179 - "Clean" 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 180 - "Robot Container" 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 181 - "Dist Download Utility" 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 182 - "Cloudflare Pages Function.test" 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 183 - "PathPlanner Integration" 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 184 - "Logging" 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 185 - "Metrics.test" 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" -Cohesion: 0.24 -Nodes (10): Admin workspace backup action, Admin workspace restore action, createProjectArchive, restoreProjectArchive, runTar, isInsideDirectory, deployFileDeleteResponse, deployFileWriteResponse (+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 182 - "Cloudflare Pages Function.test" +### Community 186 - "004 Backend Wiring" Cohesion: 0.2 Nodes (10): readError, useAutoChoosers test suite, useAutoChoosers, useContainerStatus test suite, useContainerStatus, probeEditor, useEditorReachability test suite, useEditorReachability (+2 more) -### Community 183 - "PathPlanner Integration" +### Community 187 - "Store" +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 188 - "Dist Download" 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) -### Community 184 - "Logging" -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 189 - "Img Screenshots" +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 185 - "Metrics.test" +### Community 190 - "Audit/Reconciliation Test Suite" 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 191 - "Dialog" Cohesion: 0.31 Nodes (5): connectGamepad(), installGamepadShim(), setGamepadAxes(), consoleErrors, leaseErrors -### Community 187 - "Store" +### Community 192 - "Main" Cohesion: 0.22 Nodes (7): catalog, catalogRoot, ids, manifestPath, repoRoot, subdirPath, lessonCatalogSchema -### Community 188 - "Dist Download" +### Community 193 - "Main" Cohesion: 0.25 -Nodes (6): patchCall, { result }, updatedResponse, VALID_RESPONSE, useAutoChoosers(), UseAutoChoosersReturn +Nodes (6): LoadState, heartbeatCalls, { result }, { unmount }, VALID_SESSION, useSession() -### Community 189 - "Img Screenshots" +### Community 194 - "Typecheck" Cohesion: 0.25 -Nodes (6): LoadState, heartbeatCalls, { result }, { unmount }, VALID_SESSION, useSession() +Nodes (6): patchCall, { result }, updatedResponse, VALID_RESPONSE, useAutoChoosers(), UseAutoChoosersReturn -### Community 190 - "Audit/Reconciliation Test Suite" +### Community 195 - "Project Preview & Right Pane" Cohesion: 0.22 -Nodes (8): bridge, enabledDisable, flush, joystick, messages, msg, sockets, zeroButtons +Nodes (8): bashCalls, cloneCall, ctx, importer, mock, row, workspace, GithubImportContext -### Community 191 - "Dialog" +### Community 196 - "Global Setup" +Cohesion: 0.39 +Nodes (8): Args, backupDatabase(), dirExists(), fileExists(), main(), parseArgs(), runTar(), timestamp() + +### Community 197 - "Docusaurus.config" Cohesion: 0.22 -Nodes (4): IdleManager, IdleManagerOptions, log, getLogger() +Nodes (8): bridge, enabledDisable, flush, joystick, messages, msg, sockets, zeroButtons -### Community 192 - "Main" +### Community 198 - "Sidebars" +Cohesion: 0.36 +Nodes (8): defaultDockerRunner(), dockerError(), dockerPortBindError(), inspectContainer(), inspectContainerOrThrow(), inspectContainers(), runDocker(), runDockerCli() + +### Community 199 - "Web App Shell & Theming" 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 -### Community 193 - "Main" +### Community 200 - "Constants" 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 -### Community 194 - "Typecheck" +### Community 201 - "Vite.config" 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 195 - "Project Preview & Right Pane" +### Community 202 - "Audit/Reconciliation Test Suite" Cohesion: 0.22 Nodes (8): Alternatives considered, Auth, Cardinality discipline, Consequences, Context, Decision, Decision 023: Metrics and observability via Prometheus + Grafana Cloud, Status -### 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) - -### 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) - -### 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) - -### Community 199 - "Web App Shell & Theming" +### Community 203 - "Icon Rail" 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) -### Community 201 - "Vite.config" +### Community 205 - "Scroll Area" Cohesion: 0.29 Nodes (6): PathPlannerPane, PathPlannerPaneProps, first, frame, { rerender }, second -### Community 202 - "Audit/Reconciliation Test Suite" +### Community 206 - "Use Gamepad Channel" +Cohesion: 0.25 +Nodes (6): cloneCall, copyCall, importer, mock, samples, workspace + +### Community 207 - "Cleanup Containers" 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 -### Community 203 - "Icon Rail" +### Community 208 - "Dist Download Utility" 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 244 - "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 245 - "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" -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 246 - "Dropdown Menu" Cohesion: 0.4 Nodes (4): { result }, { unmount }, VALID_STATUS, useContainerStatus() -### Community 245 - "Dropdown Menu" -Cohesion: 0.33 -Nodes (5): dockerfile, initScript, repoRoot, robotSettings, settings - -### 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): dockerfile, initScript, repoRoot, robotSettings, settings ### 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: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 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 (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 250 - "Resizable" Cohesion: 0.33 @@ -1397,40 +1400,40 @@ Nodes (6): code:bash (docker exec cr-smoke sh -c 'cat /config/data/User/settings ### Community 251 - "Separator" Cohesion: 0.33 -Nodes (5): 020 - Workspace runtime provider boundary, Consequences, Context, Decision, Status +Nodes (5): Background — verified findings, Follow-up disposition (closed 2026-08-26), Global Constraints, Out of scope, VSCodium-web Editor Migration Implementation Plan ### Community 252 - "Sonner" Cohesion: 0.33 -Nodes (5): 024 — Bound Per-Workspace Container Memory, Consequences, Context, Decision, Status +Nodes (5): 020 - Workspace runtime provider boundary, 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): 024 — Bound Per-Workspace Container Memory, Consequences, Context, Decision, Status ### 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): 035 — Multi-Arch Images and CI/Release/Deploy Workflow Split, Consequences, Context, Decision, Status ### Community 255 - "Tabs" Cohesion: 0.33 -Nodes (5): 025 — Detach the Simulation JVM from Gradle, Consequences, Context, Decision, Status +Nodes (5): 034 — Demo mode portability on Docker Desktop, Consequences, Context, Decision, What was deliberately not done ### 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) +Cohesion: 0.33 +Nodes (5): 025 — Detach the Simulation JVM from Gradle, Consequences, Context, Decision, Status ### 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 -### Community 261 - "Tooltip" +### Community 259 - "Tooltip" +Cohesion: 0.4 +Nodes (4): cookie, frames, snap, status + +### 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 @@ -1457,11 +1460,11 @@ Nodes (4): 010 - Gradle project cache isolation for sim and LSP, Context, Decisi ### Community 270 - "Community 270" Cohesion: 0.4 -Nodes (5): Decision 018 (gamepad unplug safety), listConnectedGamepads, makeLabel, useGamepad test suite, useGamepad +Nodes (5): Admin workspace backup action, Admin workspace restore action, createProjectArchive, restoreProjectArchive, runTar ### 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 +1496,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 @@ -1541,10 +1544,6 @@ Nodes (3): Button, DialogContent, DialogFooter ### Community 296 - "Community 296" Cohesion: 0.67 -Nodes (3): Classroom-density memory defaults, Decision 024: Container Memory Budget, VS Code Gradle Build Server path - -### Community 297 - "Community 297" -Cohesion: 0.67 Nodes (3): scripts/apply-ascope-patches.ts, scripts/build-ascope-lite.ts, scripts/clean.ts ## Ambiguous Edges - Review These @@ -1566,9 +1565,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) +- **2221 isolated node(s):** `workspace`, `frame`, `victimWorkspace`, `attackerWorkspace`, `console` (+2216 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. +- **89 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..ded28e83 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -61,12 +61,12 @@

Communities

-
4648 nodes · 6821 edges · 383 communities
+
4676 nodes · 6860 edges · 382 communities