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 s3 s3-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.
  • The bootstrap gives Canvas's developer key and the connector's REDIRECT_URI the same callback address, built from CC_APP_URL (seed_canvas_connector --app-url), so the two always agree.
  • 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 file-upload path has the same fallback: the upload target Canvas names has been rewritten to the browser origin too, so when that host does not resolve the bytes go to the API origin with the named host in the Host header (upload_post in canvas/transport.py). 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.