Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,30 +178,27 @@ jobs:
if-no-files-found: warn

- name: Vulnerability scan (HIGH,CRITICAL)
# Non-blocking by design — findings are reported, not merge-gating.
# The summary lines are kept so the next step can surface them.
# Non-blocking step wrapper so the evaluation step below can inspect the
# summary and fail closed with an explicit unassessed or finding error.
continue-on-error: true
run: |
docker builder prune -af || true
: > trivy-scan-summary.txt
node scripts/trivy-image-scan.mjs clinical-kb-app:ci | tee -a trivy-scan-summary.txt
node scripts/trivy-image-scan.mjs clinical-kb-worker:ci | tee -a trivy-scan-summary.txt
node scripts/trivy-image-scan.mjs clinical-kb-app:ci | tee -a trivy-scan-summary.txt || echo "Trivy scan failed (clinical-kb-app:ci): STATUS=unassessed reason=scanner_exit" | tee -a trivy-scan-summary.txt
node scripts/trivy-image-scan.mjs clinical-kb-worker:ci | tee -a trivy-scan-summary.txt || echo "Trivy scan failed (clinical-kb-worker:ci): STATUS=unassessed reason=scanner_exit" | tee -a trivy-scan-summary.txt

- name: Fail on HIGH/CRITICAL image findings outside pull requests
# Audit M20: the worker image carries the Python parsers (PyMuPDF,
# Pillow, pytesseract, docling) that read attacker-supplied uploads, and
# until now a HIGH/CRITICAL finding reached only this run's log. Pull
# request runs stay advisory (a base-image CVE is not the PR author's
# change), but the weekly schedule, main pushes and manual dispatches
# now fail on any HIGH/CRITICAL so the run shows red in the Actions tab.
# A scanner that could not run (disk pressure, docker save) is still
# only a warning.
# Audit M20 + R13: the worker image carries the Python parsers (PyMuPDF,
# Pillow, pytesseract, docling) that read attacker-supplied uploads.
# Fail closed if trivy-scan-summary.txt is empty, contains STATUS=unassessed,
# or does not contain STATUS=ok lines for BOTH clinical-kb-app:ci and
# clinical-kb-worker:ci. Never report a broken/empty scan as clean.
if: always()
run: |
set -euo pipefail
if [ ! -s trivy-scan-summary.txt ]; then
echo "::warning::Trivy produced no scan summary; image vulnerabilities were not assessed this run."
exit 0
echo "::error::Trivy scan unassessed: trivy-scan-summary.txt is empty; image vulnerabilities were not assessed."
exit 1
fi
{
echo "## Trivy image scan (HIGH,CRITICAL)"
Expand All @@ -210,6 +207,14 @@ jobs:
cat trivy-scan-summary.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
if grep -q 'STATUS=unassessed' trivy-scan-summary.txt; then
echo "::error::Trivy scan unassessed: one or more images reported STATUS=unassessed."
exit 1
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
fi
if ! grep -q '^Trivy scan completed (clinical-kb-app:ci): STATUS=ok\b' trivy-scan-summary.txt || ! grep -q '^Trivy scan completed (clinical-kb-worker:ci): STATUS=ok\b' trivy-scan-summary.txt; then
echo "::error::Trivy scan unassessed: missing STATUS=ok evidence for both clinical-kb-app:ci and clinical-kb-worker:ci."
exit 1
fi
high="$(grep -oE 'HIGH=[0-9]+' trivy-scan-summary.txt | cut -d= -f2 | paste -sd+ - | bc || echo 0)"
critical="$(grep -oE 'CRITICAL=[0-9]+' trivy-scan-summary.txt | cut -d= -f2 | paste -sd+ - | bc || echo 0)"
high="${high:-0}"
Expand Down
20 changes: 9 additions & 11 deletions .github/workflows/ops-digest.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
# Ops deep-health digest + canary liveness.
#
# Schedule is DAILY at 22:00 UTC (`0 22 * * *`), not hourly. Hybrid-RPC evidence
# steps still speak in "hourly" buckets for historical keying / future cadence
# bumps — that naming is intentional and is not the workflow cron.
#
# Keep workflow_dispatch for operator verification and the daily schedule for
# Schedule runs every 6 hours (`15 */6 * * *`) so trailing 60-minute SLO windows
# are sampled 4x/day and active warning/page alerts notify on the rolling issue.
name: Ops Digest

on:
Expand All @@ -16,8 +13,8 @@ on:
default: false
type: boolean
schedule:
# Once daily at 22:00 UTC.
- cron: "0 22 * * *"
# Every 6 hours at minute 15 UTC (4x/day sampling of 60-minute windows).
- cron: "15 */6 * * *"
Comment thread
BigSimmo marked this conversation as resolved.
Resolved

concurrency:
group: ops-digest
Expand Down Expand Up @@ -153,8 +150,6 @@ jobs:
script: |
const fs = require("fs");
const label = "ops-digest";
const publishAttentionComment =
context.eventName === "workflow_dispatch" && process.env.OPS_DIGEST_PUBLISH_ALERT_COMMENT === "true";
const canaryStale = process.env.EVAL_CANARY_STALE === "true";
const canaryMessage = process.env.EVAL_CANARY_MESSAGE || "";
let body = fs.readFileSync("digest.md", "utf8");
Expand All @@ -167,6 +162,9 @@ jobs:
const status = allowedStatuses.has(rawStatus) ? rawStatus : "unknown";
const rawSeverity = process.env.OPS_DIGEST_SEVERITY || "unknown";
const severity = allowedSeverities.has(rawSeverity) ? rawSeverity : "unknown";
const publishAttentionComment =
(context.eventName === "workflow_dispatch" && process.env.OPS_DIGEST_PUBLISH_ALERT_COMMENT === "true") ||
(context.eventName === "schedule" && (severity === "warning" || severity === "page"));
let parsedSummary;
try {
parsedSummary = JSON.parse(process.env.OPS_DIGEST_ALERT_SUMMARY || "{}");
Expand Down Expand Up @@ -227,7 +225,7 @@ jobs:
});
issueNumber = created.data.number;
}
// Attention comments notify subscribers, so scheduled runs never publish them.
// Publish attention comments on confirmed manual dispatches or scheduled warning/page breaches.
if (publishAttentionComment && (status !== "ok" || alerting)) {
await github.rest.issues.createComment({
owner: context.repo.owner,
Expand All @@ -236,7 +234,7 @@ jobs:
body: `⚠ Attention (status=${status}, alerting=${alerting}, severity=${severity}${canaryStale ? ", eval-canary-stale=true" : ""}) — ${new Date().toISOString()}\n\nAlert summary: \`${alertSummary}\`\n\n${body}`,
});
} else if (status !== "ok" || alerting) {
core.info("Attention comment skipped: manual operator confirmation was not supplied.");
core.info("Attention comment skipped: neither manual confirmation nor scheduled warning/page severity was met.");
}
if (status !== "ok") core.warning(`Ops digest status: ${status}`);
if (canaryStale) core.warning(canaryMessage || "Eval-canary liveness reported stale.");
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
# declares the matching build argument. This non-secret SHA keeps build-time
# source maps and runtime Sentry events on the same release identity.
ARG RAILWAY_GIT_COMMIT_SHA=
ARG GITHUB_SHA=
ARG SOURCE_COMMIT_SHA=
ARG COMMIT_SHA=
ARG SENTRY_RELEASE=
# Next reads this during the build to stamp navigation and static asset URLs with
# the operator-selected release identity instead of the commit SHA fallback.
ARG NEXT_DEPLOYMENT_ID=
Expand All @@ -86,7 +90,7 @@
# this builder stage's environment and readable by every script `npm run build` invokes. That,
# not the shipped artefact, is why it must be a token scoped to project release/sourcemap upload
# ONLY, never a broader-scoped one.
ARG SENTRY_AUTH_TOKEN=

Check warning on line 93 in Dockerfile

View workflow job for this annotation

GitHub Actions / Container images / build-and-verify

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG SENTRY_ORG=
ARG SENTRY_PROJECT=
ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
Expand All @@ -95,8 +99,13 @@
ENV NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_NAME=${NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_NAME}
ENV NEXT_PUBLIC_MAX_UPLOAD_MB=${NEXT_PUBLIC_MAX_UPLOAD_MB}
ENV RAILWAY_GIT_COMMIT_SHA=${RAILWAY_GIT_COMMIT_SHA}
ENV GITHUB_SHA=${GITHUB_SHA}
ENV SOURCE_COMMIT_SHA=${SOURCE_COMMIT_SHA}
ENV COMMIT_SHA=${COMMIT_SHA}
ENV SENTRY_RELEASE=${SENTRY_RELEASE}
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
ENV RAILWAY_PUBLIC_DOMAIN=${RAILWAY_PUBLIC_DOMAIN}
RUN node scripts/deploy/write-release-manifest.mjs
# The repo build script allocates an 8 GiB heap. Prefer builders with >= 10 GiB
# locally (Docker Desktop hard-fails under the RAM guard by default). CI image
# builds pass ALLOW_LOW_RAM_BUILD=1 because GitHub buildx runners report ~7–8 GiB
Expand All @@ -123,9 +132,17 @@

FROM node-base AS runner
WORKDIR /app
ARG RAILWAY_GIT_COMMIT_SHA=
ARG GITHUB_SHA=
ARG SOURCE_COMMIT_SHA=
ARG COMMIT_SHA=
ARG SENTRY_RELEASE=
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV RAILWAY_GIT_COMMIT_SHA=${RAILWAY_GIT_COMMIT_SHA}
ENV COMMIT_SHA=${COMMIT_SHA}
ENV SENTRY_RELEASE=${SENTRY_RELEASE}
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/.next ./.next
# From the build stage, not the build context: `stamp-service-worker.mjs` rewrites
Expand All @@ -141,6 +158,7 @@
# Railway deploy pre-deploy gate (docs/worker-deploy-runbook.md §0): blocks/
# observes this service's deploy until migrations this build expects are live.
COPY --from=build /app/deploy/expected-migrations.json ./deploy/expected-migrations.json
COPY --from=build /app/deploy/release-identity.json ./deploy/release-identity.json
COPY --from=build /app/scripts/deploy/await-migrations.mjs ./scripts/deploy/await-migrations.mjs
COPY --from=build /app/scripts/deploy/migration-versions.mjs ./scripts/deploy/migration-versions.mjs
COPY package.json next.config.ts ./
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,22 @@ RUN for attempt in 1 2 3; do \
sleep $((attempt * 10)); \
done
COPY . .
ARG RAILWAY_GIT_COMMIT_SHA=
ARG GITHUB_SHA=
ARG SOURCE_COMMIT_SHA=
ARG COMMIT_SHA=
ARG SENTRY_RELEASE=
ENV RAILWAY_GIT_COMMIT_SHA=${RAILWAY_GIT_COMMIT_SHA}
ENV GITHUB_SHA=${GITHUB_SHA}
ENV SOURCE_COMMIT_SHA=${SOURCE_COMMIT_SHA}
ENV COMMIT_SHA=${COMMIT_SHA}
ENV SENTRY_RELEASE=${SENTRY_RELEASE}
# supabase/migrations is only present in the build context, not the slim
# runtime image, so the deploy pre-deploy gate's manifest of migration
# versions this build expects is baked in now and copied into the runner
# below (see scripts/deploy/write-migration-manifest.mjs).
RUN node scripts/deploy/write-migration-manifest.mjs
RUN node scripts/deploy/write-release-manifest.mjs
RUN node scripts/build-worker.mjs

# Production-only node_modules for the runtime stage. The preinstall/
Expand Down Expand Up @@ -105,6 +116,11 @@ RUN /opt/docling-venv/bin/docling-tools models download --output-dir /opt/doclin
--manifest /tmp/docling-verify/model-artifacts.json \
&& chmod -R a+rX /opt/docling-models \
&& rm -rf /tmp/docling-verify
ARG RAILWAY_GIT_COMMIT_SHA=
ARG GITHUB_SHA=
ARG SOURCE_COMMIT_SHA=
ARG COMMIT_SHA=
ARG SENTRY_RELEASE=
ENV WORKER_DOCLING_PYTHON_BIN=/opt/docling-venv/bin/python
ENV DOCLING_ARTIFACTS_PATH=/opt/docling-models
ENV TORCHDYNAMO_DISABLE=1
Expand All @@ -113,11 +129,15 @@ ENV PATH="/opt/ocr-venv/bin:${PATH}"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV NODE_ENV=production
ENV RAILWAY_GIT_COMMIT_SHA=${RAILWAY_GIT_COMMIT_SHA}
ENV COMMIT_SHA=${COMMIT_SHA}
ENV SENTRY_RELEASE=${SENTRY_RELEASE}
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
# Railway deploy pre-deploy gate (docs/worker-deploy-runbook.md §0): blocks/
# observes this service's deploy until migrations this build expects are live.
COPY --from=build /app/deploy/expected-migrations.json ./deploy/expected-migrations.json
COPY --from=build /app/deploy/release-identity.json ./deploy/release-identity.json
COPY --from=build /app/scripts/deploy/await-migrations.mjs ./scripts/deploy/await-migrations.mjs
COPY --from=build /app/scripts/deploy/migration-versions.mjs ./scripts/deploy/migration-versions.mjs
COPY worker/python worker/python
Expand Down
2 changes: 1 addition & 1 deletion data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "repo-awareness-snapshot-v3",
"captured_revision": {
"committed_at": "2026-10-16"
"committed_at": "2026-10-17"
},
"routes": {
"modes": [
Expand Down
4 changes: 2 additions & 2 deletions docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ Administrator source intake also records admission events in `document_admission

`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `document_title_words`, `document_publication_approvals`, `document_corpus_access_state`, `document_corpus_access_snapshots`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `image_caption_cache`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `rag_visual_eval_cases`, `rag_visual_eval_runs`, `rag_answer_feedback`, `clinical_registry_records`, `clinical_registry_record_sources`, `clinical_quality_feedback_triage`, `clinical_quality_feedback_triage_events`, `medication_records`, `differential_records`, `source_review_events`, `user_favourites`, `user_favourite_sets`, `user_preferences`, `work_admin_paperwork`, `work_backups`, `work_booking_courses`, `work_course_bookings`, `work_hospitals`, `work_hospital_teams`, `work_role_grants`, `alert_reminder_times`, `alert_brief_sent`, `api_rate_limits`, `api_rate_limit_subjects`, `audit_logs`, `storage_cleanup_jobs`, `on_call_entries`, `cme_years`, `cme_requirements`, `cme_routines`, `cme_entries`, `cme_allocations`, `cme_evidence`, `cme_year_snapshots`, `cme_year_amendments`, `cme_plan_goals`, `cme_plan_goal_carries`, `cme_entry_goals`, `cme_training_periods`, `cme_training_milestones`, `cme_missed_sessions`, `cme_entry_drafts`, `calendar_feed_tokens`, `on_call_shift_imports`, `on_call_shifts`, `on_call_services`, `on_call_service_sites`, `on_call_service_members`, `on_call_service_invitations`, `on_call_service_entries`, `on_call_service_reports`, `on_call_service_orientation`, `site_content_publications`, `site_content_reconciliation_plans`, `site_content_public_records`, `site_content_sync_state`, `site_content_sync_events`, `site_content_sync_event_plans`, `site_content_sync_worker_invocations`, `site_content_releases`, `site_content_release_records`, `site_content_release_receipts`

Team modes (Roster, Admin, Teaching): `admin_leave_balances`, `admin_settings`, `extra_time_records`, `on_call_service_member_events`, `registrar_assessments`, `registrar_training_milestones`, `roster_assignments`, `roster_calendar_links`, `roster_change_agreements`, `roster_changes`, `roster_draft_assignments`, `roster_drafts`, `roster_leave`, `roster_member_roles`, `roster_open_post_keys`, `roster_open_shifts`, `roster_publication_protections`, `roster_publication_seen`, `roster_publications`, `roster_rotation_preferences`, `roster_rotation_rounds`, `roster_shift_codes`, `roster_staffing_needs`, `roster_swaps`, `roster_team_settings`, `roster_unavailability`, `teaching_attendance`, `teaching_audit_events`, `teaching_calendar_optins`, `teaching_checkin_claims`, `teaching_collection_sections`, `teaching_collections`, `teaching_display_links`, `teaching_feedback_answers`, `teaching_feedback_replied`, `teaching_group_members`, `teaching_groups`, `teaching_member_roles`, `teaching_notice_reads`, `teaching_notices`, `teaching_occurrences`, `teaching_readiness`, `teaching_resource_saves`, `teaching_resources`, `teaching_series`, `teaching_supervision_entries`, `teaching_supervision_notes`, `teaching_supervision_pairings`, `teaching_team_settings`, `teaching_week_adds`, `web_push_subscriptions`.
Team modes (Roster, Admin, Teaching): `admin_leave_balances`, `admin_settings`, `extra_time_records`, `on_call_service_member_events`, `registrar_assessments`, `registrar_training_milestones`, `roster_assignments`, `roster_calendar_links`, `roster_change_agreements`, `roster_changes`, `roster_draft_assignments`, `roster_drafts`, `roster_leave`, `roster_member_roles`, `roster_open_post_alerts`, `roster_open_post_keys`, `roster_open_shifts`, `roster_publication_protections`, `roster_publication_seen`, `roster_publications`, `roster_rotation_preferences`, `roster_rotation_rounds`, `roster_shift_codes`, `roster_staffing_needs`, `roster_swaps`, `roster_team_settings`, `roster_unavailability`, `teaching_attendance`, `teaching_audit_events`, `teaching_calendar_optins`, `teaching_checkin_claims`, `teaching_collection_sections`, `teaching_collections`, `teaching_display_links`, `teaching_feedback_answers`, `teaching_feedback_replied`, `teaching_group_members`, `teaching_groups`, `teaching_member_roles`, `teaching_notice_reads`, `teaching_notices`, `teaching_occurrences`, `teaching_readiness`, `teaching_resource_saves`, `teaching_resources`, `teaching_series`, `teaching_supervision_entries`, `teaching_supervision_notes`, `teaching_supervision_pairings`, `teaching_team_settings`, `teaching_week_adds`, `web_push_subscriptions`.

Public-source control-plane tables: `public_source_policy_entries`, `public_source_activation_events`, `public_source_versions`, `public_source_upload_attempts`, `public_source_activation_guards`, `public_source_cleanup_mutation_guards`.

Expand Down Expand Up @@ -1055,7 +1055,7 @@ Public-source control-plane tables: `public_source_policy_entries`, `public_sour

Golden retrieval fixture: `scripts/fixtures/rag-retrieval-golden.json` (other RAG, adversarial and ranking-snapshot fixtures sit beside it).

Subfolders: `scripts/lib/` (shared helpers, including the protected `clinical-aliases.ts`), `scripts/deploy/` (`await-migrations.mjs` Railway pre-deploy, `write-migration-manifest.mjs`), `scripts/sql/` (verify and parity SQL), `scripts/archive/` (historical, still referenced by two checks). `docs/scripts-index.md` is the full catalogue.
Subfolders: `scripts/lib/` (shared helpers, including the protected `clinical-aliases.ts`), `scripts/deploy/` (`await-migrations.mjs` Railway pre-deploy, `write-migration-manifest.mjs`, `write-release-manifest.mjs`), `scripts/sql/` (verify and parity SQL), `scripts/archive/` (historical, still referenced by two checks). `docs/scripts-index.md` is the full catalogue.

---

Expand Down
Loading
Loading