Skip to content

Local Canvas LMS Testing

The dev stack can run a real Canvas LMS next to CampusCore, wired to the same Keycloak realm, so connector work is driven against a live instance with real OAuth, real role enforcement, and seeded coursework. One identity - one Keycloak user - logs into both apps, and the CampusCore Canvas connector is pre-configured against the stack's own Canvas.

What's included

  • Compose services behind the canvas profile: canvas-web, canvas-jobs (delayed_job), canvas-postgres (Canvas's tuned pgvector image), canvas-redis, and canvas-mailcatcher (catches all Canvas email).
  • Seeded personas, shared with the Keycloak realm (all Keycloak passwords test123; Canvas fallback password canvas123): students sofia.reyes, marcus.chen, amara.okafor, liam.novak; teachers david.kim (CS101), elena.vasquez (MATH201, HIST110); TA taylor.brooks (CS101, both sections); admin dana.whitfield; observer olivia.reyes (Sofia's parent, observing her in CS101 and MATH201); plus the Canvas-local admin admin@campus.edu / test123.
  • Seeded coursework in three courses (CS101, MATH201, HIST110): assignments in every state (graded with comments, partially graded with missing submissions, upcoming), announcements, discussions with replies, calendar events, modules, pages, files, and inbox conversations.
  • Deeper coursework for the governed action surface: weighted assignment groups in every course (Problem Sets 50%, Projects 30%, Quizzes 20%) so grade-projection math is hand-checkable; one published classic quiz per course (4 questions, 8 points), with Sofia holding a completed graded attempt on the CS101 quiz (6/8); a 3-criterion grading rubric attached to "CS101 Problem Set 3"; a "Project Teams" group category in CS101 (Team Alpha: Sofia + Marcus, with a group discussion; Team Beta: Amara); peer reviews on "MATH201 Problem Set 3" with real submitted drafts (Sofia reviews Liam, Liam reviews Sofia); a second CS101 section ("CS101 Lab") holding Marcus and TA Taylor; the past term "Spring 2026" with the concluded course CS100 (final grades for Sofia 91 and Marcus 84); and the manual-posting assignment "CS101 Essay 1" with graded-but-unposted submissions for post/hide-grades flows.
  • A Canvas OIDC auth provider pointing at the stack's Keycloak (login_attribute=email, JIT provisioning on), and per-persona pseudonyms bound to it - OIDC logins match ONLY provider-bound pseudonyms, which is why the seed creates two pseudonyms per user (password door + SSO door).
  • A CampusCore Connector developer key (unscoped, so tokens inherit the connecting user's own Canvas permissions) and the CampusCore-side ConnectorConfig rows (BASE_URL, split-horizon API_BASE_URL=http://canvas-web, credentials, blank SCOPES).

Bring it up

# from a worktree (implies --sso):
bash scripts/stack.sh up --canvas

# or on the main checkout:
docker compose --profile canvas --profile sso up -d db minio minio-init elasticmq redis web worker web_ui keycloak canvas-postgres canvas-redis canvas-mailcatcher
bash scripts/canvas_bootstrap.sh

CANVAS_LMS_DIR must point at a canvas-lms checkout (default ~/codes/canvas-lms) - the dev image carries no source; the checkout is bind-mounted and gems/assets live in named volumes. The first bootstrap on a cold cache installs gems, JS deps, and compiled assets (up to an hour); the caches are shared machine-wide (canvas-lms_* volumes), so later stacks and re-runs are minutes. Do not bootstrap Canvas in two stacks at the same time - the shared caches are single-writer. Canvas wants real memory: budget roughly 3-4 GiB of Docker RAM for the Canvas services on top of the stack's usual footprint.

The up summary prints the URLs: Canvas at http://cc-<slug>.localhost:<base+10>, mailcatcher UI at localhost:<base+11>.

How the networking works

  • The browser reaches Canvas at the published port; Keycloak issues tokens whose issuer is the browser-facing URL, while Canvas fetches discovery/JWKS over the compose network (keycloak:8080) - the same split-horizon design the app's own SSO uses.
  • CampusCore's server-side calls (token exchange, every connector API call) reach Canvas at http://canvas-web - that is the connector's API_BASE_URL, while BASE_URL stays browser-facing for the OAuth redirect. In production both are one public URL and API_BASE_URL stays blank.
  • Canvas builds every URL in an API response from the request's Host header, so server-side calls would hand the user dead http://canvas-web/... links. The connector rewrites response bodies from the API origin to BASE_URL at the client boundary (rewrite_response_origin in canvas/transport.py), and the file-download path treats both origins as the same Canvas so the authed no-verifier first hop still fires. The rewrite activates only for connections stamped split_horizon at connect time whose configured origins still differ - in production they coincide and responses pass through untouched (pinned by TestSplitHorizonUrlRewrite).
  • The mounted initializer infrastructure/canvas/config/zz_campuscore_dev_stack.rb clears CanvasHttp's SSRF guard (every compose peer is a private IP) and allows the canvas-web Host - both dev-stack-only concessions.

Driving the connector

Log into CampusCore via Keycloak as any persona, connect Canvas under Settings -> Connectors (the Canvas consent screen shows which Canvas user is authorizing - if it names the wrong person, log that user out of Canvas first), then ask the assistant:

  • Student (sofia.reyes): "what are my grades", "what do I still need to submit", "submit my CS101 Problem Set 3 with this text: ..." - the write pauses on an approve/deny card.
  • Teacher (david.kim): "who hasn't submitted Problem Set 2", "grade Sofia's Problem Set 3 with 48/50 and a comment" - grading is the teacher's own authority in Canvas; the card confirms it.
  • Admin-gated actions (enrollment changes) refuse from chat - they are admin_approve and no approval queue exists yet.

Further persona walkthroughs, all verified against this seed:

  • Sofia: "what quizzes do I have in CS101 and how did I do" (6/8 on the CS101 quiz, metadata only - no question content exists for any caller); "what's new in my courses" (activity stream); "what do I need to average on remaining work for an A in MATH201" (89.2% from the 50/30/20 weights); "do I have any peer reviews" (Liam's PS3 draft, review left as a submission comment via the card); "what's happening in my project group" (Team Alpha's discussion); "reply to the CS101 check-in" (inbox thread reply via the card).
  • Sofia: "read the MATH201 reading list file and quote its first item" - the agent loads the course file into the chat (load_course_file_into_chat), reads it the same turn, and the file appears in the conversation's Context panel (never as a message chip). A second seeded student (liam.chen) asking for the same file gets an instant link to the already-processed copy - one ingestion run total, visible in the Pipeline Manager.
  • Elena (elena.vasquez, MATH201): "give Sofia two extra days on Problem Set 3" (an assignment override); "everyone who submitted PS3 gets 45/50" (one bulk-grade card, grades land through Canvas's background batch); "message everyone who hasn't submitted PS2" (recipients resolved at send time; the receipt names exactly who got it).
  • David (CS101): read the PS3 rubric and grade a student per-criterion against it; "did my Essay 1 grades reach students" (manual-posting course - graded work stays invisible until posted; posting runs the GraphQL mutation); attach a PDF in chat and "add this to the CS101 files" (the card names the file; the three-step upload puts it in course files); build and publish a "Week 2" module holding the quiz; "who's falling behind" (session activity + missing-work counts, stalest first).
  • Olivia (olivia.reyes): log into Canvas through Keycloak - her dashboard shows CS101 and MATH201 as "Observing: Sofia Reyes".
  • Dana (dana.whitfield): Settings -> Workspace Connectors -> Canvas shows every action as a governed row (topic groups, read/write badges, role scopes); "Disable all writes" is one click, and a disabled action disappears from the agent and refuses if forced.

Canvas enforces roles upstream: a teacher calling the account-level admin searches gets a 403 that surfaces as "your Canvas role is not allowed", not as a broken connection. On top of that, the institution's action policies gate what each role may use at all - the dev seed enables everything for every role; production defaults are reads-on, writes-off (see connectors.md).

Notes

  • Everything here is dev-only. The seed command refuses on IS_CLOUD_ENV; the realm, secrets, and passwords are fixtures.
  • The Canvas seed is idempotent (infrastructure/canvas/seed_canvas_data.rb); re-running stack.sh up --canvas converges and re-anchors assignment due dates to "now" - except the concluded CS100, which is left untouched once completed.
  • The canvas-lms checkout is used as-is on whatever commit it sits at; the stack was built against 1c9f0bb8013 (master, 2026-04-30).
  • Canvas data lives in the per-stack canvas_pgdata volume; stack.sh down removes it with the rest of the stack.