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
canvasprofile:canvas-web,canvas-jobs(delayed_job),canvas-postgres(Canvas's tuned pgvector image),canvas-redis, andcanvas-mailcatcher(catches all Canvas email). - Seeded personas, shared with the Keycloak realm (all Keycloak passwords
test123; Canvas fallback passwordcanvas123): studentssofia.reyes,marcus.chen,amara.okafor,liam.novak; teachersdavid.kim(CS101),elena.vasquez(MATH201, HIST110); TAtaylor.brooks(CS101, both sections); admindana.whitfield; observerolivia.reyes(Sofia's parent, observing her in CS101 and MATH201); plus the Canvas-local adminadmin@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 Connectordeveloper key (unscoped, so tokens inherit the connecting user's own Canvas permissions) and the CampusCore-sideConnectorConfigrows (BASE_URL, split-horizonAPI_BASE_URL=http://canvas-web, credentials, blankSCOPES).
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'sAPI_BASE_URL, whileBASE_URLstays browser-facing for the OAuth redirect. In production both are one public URL andAPI_BASE_URLstays blank. - The bootstrap gives Canvas's developer key and the connector's
REDIRECT_URIthe same callback address, built fromCC_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 toBASE_URLat the client boundary (rewrite_response_originincanvas/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 theHostheader (upload_postincanvas/transport.py). The rewrite activates only for connections stampedsplit_horizonat connect time whose configured origins still differ - in production they coincide and responses pass through untouched (pinned byTestSplitHorizonUrlRewrite). - The mounted initializer
infrastructure/canvas/config/zz_campuscore_dev_stack.rbclears CanvasHttp's SSRF guard (every compose peer is a private IP) and allows thecanvas-webHost - 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_approveand 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-runningstack.sh up --canvasconverges 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_pgdatavolume;stack.sh downremoves it with the rest of the stack.