Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
a7c455e
docs(spec): add §9 hardening pass to ai-assistant spec
lyubomir-bozhinov Jun 19, 2026
8249fea
docs(assistant): add agent-team implementation addendum
sangelov-tradu Jun 20, 2026
0d3840d
docs(assistant): state report guarantees vs. limits
sangelov-tradu Jun 20, 2026
b9f4a77
docs(assistant): add correctness guardrails (A–F)
sangelov-tradu Jun 20, 2026
c9e6427
docs(assistant): apply multi-agent review fixes (15 findings)
sangelov-tradu Jun 20, 2026
ca1195f
Merge remote-tracking branch 'upstream/main' into feat/ai-assistant
lyubomir-bozhinov Jun 22, 2026
38566e8
Merge remote-tracking branch 'upstream/main' into feat/ai-assistant
lyubomir-bozhinov Jun 23, 2026
321fcf8
feat(assistant): HMAC-sign server transcript messages (E1)
ydimitrof Jun 23, 2026
94f684b
feat(assistant): server-side trim/summarize of old turns (E2)
ydimitrof Jun 23, 2026
c66c521
feat(assistant): default query filters guard (E3)
ydimitrof Jun 23, 2026
21e69f9
feat(assistant): reconcile-with-rollup guard (E4)
ydimitrof Jun 23, 2026
a773fcc
feat(assistant): CPV interpretation mapping guard (E5)
ydimitrof Jun 23, 2026
d9c8c33
fix(assistant): drop malformed-slot transcript messages instead of th…
ydimitrof Jun 23, 2026
8914724
feat(assistant): bind report chips into HMAC tuple and re-verify trim…
ydimitrof Jun 23, 2026
6b518ba
fix(assistant): align E3 default filter to the rollup row-set (amount…
ydimitrof Jun 24, 2026
57be0b6
test(assistant): add resetKeyCache() seam to drop HMAC key-cache betw…
ydimitrof Jun 24, 2026
f299b05
fix(assistant): scope E4 to amount_eur rollups, drop dead synthetic g…
ydimitrof Jun 24, 2026
f1489f1
test(assistant): guard cpv-map hardcoded synonym codes against config…
ydimitrof Jun 24, 2026
0fe761c
Merge pull request #3 from lyubomir-bozhinov/feat/integrity-anti-inje…
lyubomir-bozhinov Jun 24, 2026
2a32cf1
Merge remote-tracking branch 'upstream/main' into feat/ai-assistant
lyubomir-bozhinov Jun 26, 2026
bf29823
Merge remote-tracking branch 'upstream/main' into HEAD
lyubomir-bozhinov Jun 28, 2026
ac368d2
feat(assistant): add the renderer/persist/dock contract seam
lyubomir-bozhinov Jun 28, 2026
65582b4
style: prettier-format seam fixtures and merge-surfaced files
lyubomir-bozhinov Jun 28, 2026
98c3da7
Report dedup lane (F1–F3): L0–L3 keys, single-flight coordinator, str…
lyubomir-bozhinov Jun 28, 2026
dd4bab4
style: prettier-format dedup lane for the integration branch config
lyubomir-bozhinov Jun 28, 2026
2a33bd6
build(web): add assistant-dock deps and a jsdom test project
nikimilenkov Jun 25, 2026
d4c5831
feat(web): add always-on AI assistant chat dock
nikimilenkov Jun 25, 2026
7d047bf
fix(web): align assistant dock with the real report-schema contract
nikimilenkov Jun 25, 2026
708c888
Revert "fix(web): align assistant dock with the real report-schema co…
nikimilenkov Jun 25, 2026
dfc9fc4
feat(web): add bar.key and timeseries multi-series to the report cont…
nikimilenkov Jun 25, 2026
975bc29
fix(web): close assistant-dock WCAG 2.2 gaps (focus-not-obscured + pl…
nikimilenkov Jun 25, 2026
9c797b0
docs(web): note trimMessages keeps a lone over-budget turn (server 41…
nikimilenkov Jun 26, 2026
0b17def
Merge pull request #5 from lyubomir-bozhinov/feat/assistant-dock
nikimilenkov Jun 29, 2026
43daa4c
feat(assistant): golden-reports CI harness + adversarial SQL-guard/op…
ydimitrof Jun 30, 2026
ac37dde
Merge pull request #8 from lyubomir-bozhinov/test/ai-assistant-dock
nikimilenkov Jun 30, 2026
54ed11b
feat(assistant): route model traffic through Cloudflare AI Gateway (O…
cefothe Jul 1, 2026
54e3e7e
feat(assistant): tolerant emit_report normalization (port from #9)
lyubomir-bozhinov Jul 1, 2026
9dcaaea
feat(web): Lane D report rendering & UI (#19)
DiyanaDimitrova Jul 1, 2026
f2a0a15
feat(assistant): token-gated /assistant/reindex schema-corpus seed route
cefothe Jul 1, 2026
fc9a0e1
chore(web): keep the fork's concrete gateway env config off upstream …
lyubomir-bozhinov Jul 1, 2026
562a4e9
fix(web): UI/UX polish and data privacy for Lane D report pages (#20)
DiyanaDimitrova Jul 2, 2026
6b2283c
Merge pull request #22 from lyubomir-bozhinov/feat/deterministic-date…
cefothe Jul 2, 2026
7854595
fix(assistant): stop the prose-number gate rejecting legit Bulgarian …
cefothe Jul 2, 2026
3181d97
feat(assistant): add find_entity tool for Cyrillic-safe entity lookup
cefothe Jul 2, 2026
77507ef
Merge pull request #23 from lyubomir-bozhinov/feat/assistant-history-…
lyubomir-bozhinov Jul 2, 2026
28fe1d0
style(assistant): prettier-format the find_entity data-dictionary rule
cefothe Jul 2, 2026
320a672
feat(assistant): nudge a leading totals headline so list reports show…
nikimilenkov Jul 2, 2026
b0fc60b
fix(assistant): scope the totals-headline rule to table/bar and pin s…
nikimilenkov Jul 2, 2026
15f4691
Merge pull request #31 from lyubomir-bozhinov/feat/assistant-report-f…
lyubomir-bozhinov Jul 3, 2026
f8d90e1
Merge pull request #24 from lyubomir-bozhinov/feat/assistant-stream-p…
lyubomir-bozhinov Jul 3, 2026
146c964
feat(assistant): Turnstile edge gate on /assistant/chat (H3, server-s…
Jul 3, 2026
b44e9ed
test(assistant): type-safe turnstile test — capture body in the fetch…
Jul 3, 2026
14defd5
Merge remote-tracking branch 'upstream/main' into sync-feat
lyubomir-bozhinov Jul 3, 2026
96a1e67
Merge pull request #35 from lyubomir-bozhinov/feat/assistant-turnstil…
lyubomir-bozhinov Jul 4, 2026
aaeca6f
fix(web): report page bug fixes — mobile dock and partial column bind…
DiyanaDimitrova Jul 5, 2026
fce2680
Merge pull request #41 from lyubomir-bozhinov/feat/mirror-36-to-feat
lyubomir-bozhinov Jul 5, 2026
595a55c
feat(assistant): risk-scaled LLM verifier (role ④) behind the determi…
ydimitrof Jul 5, 2026
a094734
Merge pull request #42 from lyubomir-bozhinov/feat/mirror-38-to-feat
lyubomir-bozhinov Jul 5, 2026
49fcbc9
feat(assistant): dock markdown, report dedup (Lane F), and launch-gat…
lyubomir-bozhinov Jul 5, 2026
20534f5
Merge pull request #43 from lyubomir-bozhinov/feat/mirror-37-to-feat
lyubomir-bozhinov Jul 5, 2026
0402d47
Merge upstream/main into feat/ai-assistant (sync 2026-07-06)
lyubomir-bozhinov Jul 5, 2026
5d22bb0
Merge pull request #45 from lyubomir-bozhinov/sync/feat-2026-07-06
lyubomir-bozhinov Jul 5, 2026
133f1eb
feat(assistant): gateway-429 shed (#47) + WCAG 2.2 AA pass (#48)
lyubomir-bozhinov Jul 6, 2026
4373e3c
Merge pull request #50 from lyubomir-bozhinov/feat/mirror-47-48-to-feat
lyubomir-bozhinov Jul 6, 2026
e79e435
feat(assistant): mandate report details and unify insufficient-data m…
lyubomir-bozhinov Jul 6, 2026
74297cc
feat(assistant): route AI Gateway to BgGPT (mamay) custom provider (#55)
lyubomir-bozhinov Jul 6, 2026
f594424
docs(assistant): reconcile RAG + circuit-breaker notes with shipped c…
lyubomir-bozhinov Jul 6, 2026
9eed866
fix(assistant): canonical queries use is_synthetic flag, drop tenders…
DiyanaDimitrova Jul 7, 2026
138f30e
feat(assistant): client Turnstile bot gate — invisible widget + token…
mdaskalo Jul 7, 2026
6bed781
fix(assistant): render reuse affordance for a dedup cache hit (#56)
lyubomir-bozhinov Jul 7, 2026
b9a456c
fix(assistant): refuse hollow single-row fallback reports
lyubomir-bozhinov Jul 8, 2026
0dd6be9
fix(assistant): add answer_directly escape hatch so non-data turns sk…
lyubomir-bozhinov Jul 8, 2026
d9d4f1f
fix(assistant): SQL guard fail-closed allowlist + amplification bound…
tradu-lboz Jul 8, 2026
39141cc
feat(assistant): voice input lane /assistant/transcribe (feat mirror …
tradu-lboz Jul 8, 2026
b354cec
feat(assistant): HMAC transcript signing §9.3 (feat mirror of #68)
tradu-lboz Jul 8, 2026
fe68fa3
Merge pull request #71 from lyubomir-bozhinov/feat/mirror-fallback-qu…
lyubomir-bozhinov Jul 8, 2026
23da94b
Merge pull request #72 from lyubomir-bozhinov/feat/mirror-sql-security
lyubomir-bozhinov Jul 8, 2026
956884a
Merge pull request #73 from lyubomir-bozhinov/feat/mirror-voice
lyubomir-bozhinov Jul 8, 2026
20c1a3d
Merge pull request #74 from lyubomir-bozhinov/feat/mirror-hmac
lyubomir-bozhinov Jul 8, 2026
3b9fce8
mirror(assistant): reflect #75 review + #77 report-quality fixes to u…
lyubomir-bozhinov Jul 9, 2026
2bf8d39
deploy(assistant): reflect assistant deploy integration onto upstream…
lyubomir-bozhinov Jul 9, 2026
7e782b3
docs(assistant): sync implementation README with the shipped feature
lyubomir-bozhinov Jul 9, 2026
0edb84a
style(assistant): prettier-format README (fixes CI lint)
lyubomir-bozhinov Jul 9, 2026
f84065a
fix(assistant): address correctness + secret-hygiene review findings
lyubomir-bozhinov Jul 10, 2026
c8a0e7d
fix(assistant): hide internal entity-id scheme in cells and bound rec…
lyubomir-bozhinov Jul 10, 2026
b091d73
fix(assistant): collapse composite contract ids so the embedded bidde…
lyubomir-bozhinov Jul 10, 2026
5e1a767
fix(assistant): exclude user-message report chips from the signed sum…
lyubomir-bozhinov Jul 11, 2026
82a779f
Merge upstream/main into feat/ai-assistant (sync 2026-07-16)
lyubomir-bozhinov Jul 16, 2026
0c6f146
feat(ci): add per-account AI-Gateway + Turnstile render knobs to wran…
cefothe Jul 16, 2026
427b1b6
fix(ci): drop docs/ path from wrangler-render comment to satisfy docs…
lyubomir-bozhinov Jul 16, 2026
57db887
Merge upstream/main into feat/ai-assistant (sync 2026-07-17)
lyubomir-bozhinov Jul 17, 2026
1f59a57
Merge upstream/main into feat/ai-assistant (sync 2026-07-29)
lyubomir-bozhinov Jul 29, 2026
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
27 changes: 27 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,30 @@

# Web worker privacy-preserving request log IP HMAC key.
LOG_IP_KEY=dev-only-log-ip-key-change-me

# ── AI assistant (docs/spec/ai-assistant.md) ───────────────────────────────────────────────────────
# Provider API key (OpenRouter today) — SECRET. Without it /assistant/chat returns a controlled 503
# (endpoint unprovisioned). In the cloud set via `wrangler secret put ASSISTANT_API_KEY --name <worker>`,
# never committed.
# ASSISTANT_API_KEY=
#
# All model traffic MUST transit the Cloudflare AI Gateway (fail closed): without AI_GATEWAY_BASE_URL the
# assistant returns 503 rather than calling the provider directly. Set both to your gateway (account
# <account-id>); AI_GATEWAY_ID is the gateway slug used to route Workers AI embeddings.
# ASSISTANT_MODEL is the provider-scoped model id — swap models by editing it alone.
# AI_GATEWAY_BASE_URL=https://gateway.ai.cloudflare.com/v1/<account-id>/<gateway>/openrouter/v1
# AI_GATEWAY_ID=<gateway>
# ASSISTANT_MODEL=google/gemma-4-31b-it # append ':free' for the rate-limited free tier
#
# NOTE: Workers AI (`AI`) and Vectorize (`VECTORIZE`) have NO local miniflare emulation, and
# vite.config.ts configures no remote bindings — so under a plain `pnpm dev` the assistant route
# degrades (RAG grounding/semantic_search no-op, then 503 without a key). To exercise the assistant
# locally, run with remote bindings (`wrangler dev --remote`) against the dev account's resources.
#
# Optional: enables the one-shot schema-corpus seed route POST /assistant/reindex (off when unset).
# High-entropy value; sent as `Authorization: Bearer <token>`. See app/lib/assistant/README.md.
# ASSISTANT_SEED_TOKEN=
#
# Transcript signing key (HMAC-SHA-256 over server-emitted messages).
# Prod: `wrangler secret put ASSISTANT_HMAC_KEY`. Never commit the real value.
ASSISTANT_HMAC_KEY=dev-only-assistant-hmac-key-change-me
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ jobs:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
# Pinned to an exact minor: node:sqlite bundles SQLite, whose version (and thus the EXPLAIN
# opcode universe the assistant opcode-guard test pins) changes with the Node minor. 22.23.0
# ships SQLite 3.51.3, which READ_ONLY_OPCODES/KNOWN_SQLITE_VERSIONS cover. Bump deliberately
# and re-harvest the allowlist (see sql-opcode-guard.test.ts) rather than letting 22.x float.
node-version: 22.23.0
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Dependency audit
Expand Down Expand Up @@ -86,13 +90,29 @@ jobs:
- name: Typecheck
if: ${{ !cancelled() }}
run: pnpm typecheck
# `pnpm test` also runs the assistant adversarial suites — the two-layer SQL guard
# (sql-guard-adversarial), the EXPLAIN-opcode allowlist (sql-opcode-guard), and the
# prompt-injection boundary tests (system-prompt / tool-results / chat-input) — so a regression in
# any read-only / injection defence blocks merge (launch gate §9.9, issue #83).
- name: Test
if: ${{ !cancelled() }}
run: pnpm test
# Launch gate §9.9 (issue #83): replays the golden report fixtures through the real bind/reconcile
# pipeline and asserts A–E2 (canonical amount_eur, default filters, rollup reconciliation, no prose
# figures). Blocking, so a model / schema / prompt change can't silently regress a published report.
- name: Golden reports
if: ${{ !cancelled() }}
run: pnpm test:golden
# Docs integrity (#102): fail on a dangling docs reference in source or an
# unindexed doc. The checker self-tests first, so the gate is itself gated.
- name: Docs integrity
if: ${{ !cancelled() }}
run: |
pnpm check:docs:test
pnpm check:docs
# Root-level tooling (scripts/*.mjs) lives outside the turbo/vitest workspaces, so `pnpm test` never
# ran its node:test suites. Gate them here — the CI-provisioning + teardown tools (ensure-kv-namespace,
# ensure-voice-provider, teardown-remote, reap-previews, load-eop) are deploy-critical and must stay covered.
- name: Script tests
if: ${{ !cancelled() }}
run: node --test scripts/*.test.mjs
99 changes: 89 additions & 10 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ name: Deploy (web + etl → Cloudflare)
# Event → target mapping (see the `detect` job):
# - push to `main` → staging (continuous staging on every merge to main, so staging leads prod)
# - version TAG (v*) → production (cut a release: git tag v1.0.1 && git push origin v1.0.1)
# - workflow_dispatch → the chosen environment
# midt-bg/sigma is the single repo carrying both the `staging` and `production` Environments (the former
# - workflow_dispatch → the chosen environment (incl. `dev`)
# midt-bg/sigma is the single repo carrying the `dev`, `staging` and `production` Environments (the former
# midt-bg/sigma-stage mirror was consolidated into it). Production is cut deliberately as a tag; staging
# tracks `main`. (To deploy on GitHub *Releases* instead of tags, swap the tag trigger for
# `release: { types: [published] }`.)
#
# `dev` is a long-lived, manual-only target: deploy ANY branch to it on demand from the Actions tab or
# gh workflow run deploy.yml --ref <branch> -f environment=dev
# It reuses the same render/guard machinery — it just needs a `dev` GitHub Environment provisioned with
# its own SIGMA_* names + a dev D1 id. Ephemeral per-PR previews are a separate workflow (preview.yml).
# Setup runbook: docs/dev-environments.md.

on:
push:
Expand All @@ -24,7 +30,14 @@ on:
description: Deploy target
type: choice
options:
- dev
- staging
# Once provision-environments.sh has been applied, selecting `production` here only
# works when the workflow is dispatched from a `v*` tag ref, e.g.:
# gh workflow run deploy.yml --ref v1.0.1 -f environment=production
# Dispatching from a branch ref (e.g. main) is refused by the environment tag policy.
# Admins may bypass this via the `admins_can_bypass` setting (Settings → Environments
# → production) for emergency situations.
- production
default: staging

Expand All @@ -44,12 +57,19 @@ jobs:
# workflow_dispatch -> the chosen environment
# tag v* -> production
# push to main -> staging (continuous staging — main always leads prod)
# Pass the GitHub expressions through `env:` and reference the quoted shell vars, rather than
# interpolating `${{ github.ref }}` (attacker-influenceable via a crafted tag/branch name)
# directly into the script body — that would let a ref like `v"; <cmd>; "` break out of the test.
env:
GITHUB_REF: ${{ github.ref }}
EVENT_NAME: ${{ github.event_name }}
INPUT_ENV: ${{ inputs.environment }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "env=${{ inputs.environment }}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "env=$INPUT_ENV" >> "$GITHUB_OUTPUT"
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "env=production" >> "$GITHUB_OUTPUT"
elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
elif [ "$GITHUB_REF" = "refs/heads/main" ]; then
echo "env=staging" >> "$GITHUB_OUTPUT"
else
echo "env=" >> "$GITHUB_OUTPUT"
Expand All @@ -59,6 +79,11 @@ jobs:
needs: detect
if: needs.detect.outputs.env != ''
runs-on: ubuntu-latest
# Explicit cap instead of the 360-min GitHub default: a full build + two-Worker wrangler deploy
# runs in single-digit minutes, so a job past 10 min is hung (stalled network I/O, wedged
# wrangler) — fail fast rather than burn a 6-hour slot. The required-reviewers wait on
# `production` does NOT count against this; the timer starts only once the job hits a runner.
timeout-minutes: 10
environment: ${{ needs.detect.outputs.env }}
concurrency:
group: deploy-${{ needs.detect.outputs.env }}
Expand All @@ -76,6 +101,20 @@ jobs:
SIGMA_CSV_CACHE_NAME: ${{ vars.SIGMA_CSV_CACHE_NAME }}
SIGMA_REPORTS_NAME: ${{ vars.SIGMA_REPORTS_NAME }}
SIGMA_VECTORIZE_NAME: ${{ vars.SIGMA_VECTORIZE_NAME }}
# Per-build dedup freshness `c` — wrangler-render stamps this over the committed "dev" so a code-only
# deploy (report shape / FX / CPV logic) busts the report cache, and each env's build keys stay distinct.
SIGMA_BUILD_ID: ${{ github.sha }}
# Master kill-switch override (#83), per-environment: set the SIGMA_ASSISTANT_ENABLED *variable* to
# "true" on the environments that should serve the assistant (dev now; staging at go-live). Unset →
# wrangler-render leaves the committed fail-dark "false", so production stays dark until deliberately
# flipped. Killing a live assistant is then just clearing this var + redeploy (or the runtime flag).
SIGMA_ASSISTANT_ENABLED: ${{ vars.SIGMA_ASSISTANT_ENABLED }}
# Runtime deploy-env for the §9.3 HMAC gate (ADR-0012). Set the SIGMA_ENVIRONMENT *variable* per
# GitHub Environment: "production" and "staging" are public/unauthenticated → the gate REQUIRES the
# signing key (fail-closed 503 without it); any other value (or unset → committed "development")
# fails open. wrangler-render stamps this into ENVIRONMENT. Not derived from import.meta.env.PROD,
# which Vite inlines true for the staging build too (it would misclassify staging as production).
SIGMA_ENVIRONMENT: ${{ vars.SIGMA_ENVIRONMENT }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
Expand Down Expand Up @@ -121,8 +160,34 @@ jobs:
check "$SIGMA_VECTORIZE_NAME" sigma-assistant SIGMA_VECTORIZE_NAME
[ "$fail" = 0 ] || { echo "Refusing to deploy '${{ needs.detect.outputs.env }}' with production resource names."; exit 1; }

# GitOps: enforce this environment's report-dedup KV namespace from git (idempotent create-if-absent)
# and hand its id to wrangler-render via SIGMA_DEDUP_KV_ID. One namespace per real env — dev (shared
# with the ephemeral previews), staging, prod — kept distinct by title so no env can touch another's
# cache. `production` maps to the short `prod` suffix; dev/staging use the env name verbatim.
- name: Ensure DEDUP_KV namespace
if: steps.guard.outputs.ok == 'true' && success()
run: |
case "${{ needs.detect.outputs.env }}" in
production) title=sigma-dedup-prod ;;
*) title="sigma-dedup-${{ needs.detect.outputs.env }}" ;;
esac
id="$(node scripts/ensure-kv-namespace.mjs "$title")"
echo "::add-mask::$id"
echo "SIGMA_DEDUP_KV_ID=$id" >> "$GITHUB_ENV"

# GitOps: ensure the account-scoped `bggpt-voice` custom provider (+ the shared `sigma-assistant`
# gateway) the VOICE lane calls directly. Provider-only — the assistant hits the gateway's provider
# endpoints for transcription, NOT a dynamic route (dynamic routing can't carry audio; see ADR-0011).
# Idempotent: a no-op when the provider already exists. VOICE_ASSISTANT_API_KEY is used only to
# first-create the provider; absent ⇒ the existing provider is reused as-is.
- name: Ensure voice AI-Gateway provider
if: steps.guard.outputs.ok == 'true' && success()
env:
VOICE_ASSISTANT_API_KEY: ${{ secrets.VOICE_ASSISTANT_API_KEY }}
run: node scripts/ensure-voice-provider.mjs --apply

- name: Typecheck
if: steps.guard.outputs.ok == 'true'
if: steps.guard.outputs.ok == 'true' && success()
run: pnpm typecheck

# The base schema was created out-of-band with `d1 execute --file`, so wrangler's migration
Expand Down Expand Up @@ -196,11 +261,11 @@ jobs:

# `run deploy`, not `deploy` — bare `pnpm deploy` is a pnpm built-in, not our package script.
- name: Deploy explorer (sigma)
if: steps.guard.outputs.ok == 'true'
if: steps.guard.outputs.ok == 'true' && success()
run: pnpm --filter @sigma/web run deploy

- name: Initialize LOG_IP_KEY secret if absent
if: steps.guard.outputs.ok == 'true'
if: steps.guard.outputs.ok == 'true' && success()
run: |
# LOG_IP_KEY is long-lived so IP HMAC tokens remain stable for same-client correlation.
# Never overwrite it; a new blue-green worker name starts without a key and correlation
Expand Down Expand Up @@ -238,8 +303,22 @@ jobs:
fi

key="$(openssl rand -hex 32)"
# Register the value as a secret with the runner BEFORE it is used anywhere else, so an
# accidental echo / set -x trace in a later edit can never leak it. Masking is not
# retroactive within a line, hence add-mask on the line right after generation.
echo "::add-mask::$key"
printf '%s' "$key" | pnpm --filter @sigma/web exec wrangler secret put LOG_IP_KEY --name "$SIGMA_WEB_NAME"

# The §9.3 transcript-signing key (ADR-0011/0012). Same generate-if-absent story as LOG_IP_KEY above:
# a purely internal HMAC key with no human value, generated once per worker and left stable across
# redeploys (rotating it would invalidate every in-flight client transcript at once). On production +
# staging the runtime gate fails CLOSED without it, so this step must run on every deploy to keep the
# required key present. The key is generated in-process and streamed to wrangler over stdin — it never
# appears in a log line, so no ::add-mask:: is needed. Runs after the deploy: the worker must exist first.
- name: Initialize ASSISTANT_HMAC_KEY secret if absent
if: steps.guard.outputs.ok == 'true' && success()
run: node scripts/ensure-worker-secret.mjs ASSISTANT_HMAC_KEY

- name: Deploy refresh Workflow (sigma-etl)
if: steps.guard.outputs.ok == 'true'
if: steps.guard.outputs.ok == 'true' && success()
run: pnpm --filter @sigma/etl run deploy
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ worker-configuration.d.ts

# Local-only policy/regulatory source materials copied in for review (never commit)
.policy-source/

# Playwright MCP session artifacts (screenshots / a11y snapshots)
.playwright-mcp/
4 changes: 3 additions & 1 deletion apps/etl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@sigma/ingest": "workspace:*"
"@sigma/config": "workspace:*",
"@sigma/ingest": "workspace:*",
"@sigma/shared": "workspace:*"
}
}
27 changes: 27 additions & 0 deletions apps/etl/src/cron-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference types="node" />
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { PROMPTS_CRON, REFRESH_CRON } from './crons';

// Routing safety: scheduled() branches on controller.cron against the named constants. A typo in
// wrangler.toml's `crons` (or in the constants) would silently misroute a trigger, so this parses the
// committed `crons` array and asserts it equals exactly [REFRESH_CRON, PROMPTS_CRON] — a mismatch
// fails CI instead of misfiring in production.

const wranglerPath = resolve(dirname(fileURLToPath(import.meta.url)), '../wrangler.toml');

function parseCrons(toml: string): string[] {
const match = toml.match(/crons\s*=\s*\[([^\]]*)\]/);
const inner = match?.[1];
if (inner === undefined) throw new Error('no `crons = [...]` array found in wrangler.toml');
return [...inner.matchAll(/"([^"]*)"/g)].map((m) => m[1] ?? '');
}

describe('cron routing guard', () => {
it('wrangler crons equal [REFRESH_CRON, PROMPTS_CRON] in order', () => {
const crons = parseCrons(readFileSync(wranglerPath, 'utf8'));
expect(crons).toStrictEqual([REFRESH_CRON, PROMPTS_CRON]);
});
});
5 changes: 5 additions & 0 deletions apps/etl/src/crons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Cron strings shared by wrangler.toml's `crons`, scheduled()'s routing branch (index.ts), and the
// cron-guard test. Kept in a dependency-free module (no `cloudflare:workers` / `.sql` text imports) so
// the guard test can import them under plain vitest without pulling in the Workflow runtime.
export const REFRESH_CRON = '0 */6 * * *';
export const PROMPTS_CRON = '0 6 * * 1';
Loading