Skip to content

chore: sync main with sprint-29 audit fixes - #6

Merged
rafaelob merged 4 commits into
mainfrom
push-main-sync
Mar 10, 2026
Merged

chore: sync main with sprint-29 audit fixes#6
rafaelob merged 4 commits into
mainfrom
push-main-sync

Conversation

@rafaelob

Copy link
Copy Markdown
Owner

Summary

  • Merges Sprint 29A-D audit fixes (27 multi-model security, reliability, contracts, and polish improvements)
  • Syncs local main with remote main

Test plan

  • All changes already merged and validated locally

🤖 Generated with Claude Code

rafaelob and others added 4 commits March 4, 2026 11:51
…y, contracts, polish)

P0 Critical (29A): migration composite FK ordering, embedding dim=1536,
JWT revocation fail-closed with LRU cache, tenant-scoped trace/evidence stores,
setup token log redaction, aria-live SSE chat fix.

High Reliability (29B): PBKDF2 async offload, JWT startup validation,
CircuitBreaker 3-state machine (half-open probe), Redis PubSub real subscribe,
SSE client reconnection with exponential backoff, React.memo ChatMessageBubble.

Contract Alignment (29C): setup wizard endpoint/payload fix, idempotency guard,
privacy panel auth+abort, safe defaults (demo=0, dev=false), GEMINI_API_KEY alias.

Medium Polish (29D): SSE terminal ordering+backpressure, async tutor I/O,
auth-first in tutor routes, login form cleanup, api.ts type safety,
composite DB indexes, accessibility store cleanup.

Deferred: D3 (auth.py DI refactor) — follow-up sprint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 10, 2026 13:02
@rafaelob
rafaelob merged commit fa3eb4c into main Mar 10, 2026
0 of 8 checks passed
@rafaelob
rafaelob deleted the push-main-sync branch March 10, 2026 13:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR syncs main with the Sprint-29 audit-fixes branch, focusing on security/tenant isolation hardening, reliability improvements (idempotency, background work offloading), and configuration/setup polish across runtime + frontend.

Changes:

  • Tighten tenant scoping for traces and standards evidence; add idempotency guard for plan SSE runs.
  • Improve operational behavior: JWT revocation fail-closed fallback cache, async thread offloading for CPU/blocking operations, docker-compose migrations service.
  • Frontend polish: stronger typing, SSE reconnect logic, setup wizard payload mapping, accessibility store cleanup hooks.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
runtime/tests/test_redis_event_bus.py Updates tests to await Redis bus subscription.
runtime/ailine_runtime/shared/trace_store.py Makes trace reads tenant-scoped and adds tenant checks to updates.
runtime/ailine_runtime/shared/observability_store.py Keys standards evidence by (teacher_id, run_id) with backward-compatible fallback.
runtime/ailine_runtime/shared/config.py Adjusts embedding defaults, env var aliases, and production JWT validation rules.
runtime/ailine_runtime/api/routers/tutors.py Offloads blocking tutor spec/session operations to a worker thread.
runtime/ailine_runtime/api/routers/setup_service.py Stops printing setup token to stderr; tweaks embedding defaults.
runtime/ailine_runtime/api/routers/plans_stream.py Adds idempotency guard + improved heartbeat shutdown and tenant-scoped trace updates.
runtime/ailine_runtime/api/routers/observability.py Uses tenant-scoped standards evidence lookup.
runtime/ailine_runtime/api/routers/auth.py Offloads password hashing/verification to threadpool.
runtime/ailine_runtime/api/middleware/tenant_context.py Adds in-memory fallback for JTI revocation checks; shifts to fail-closed.
runtime/ailine_runtime/adapters/events/redis_bus.py Adds PubSub listener + async subscription behavior.
runtime/ailine_runtime/adapters/db/alembic/versions/2026_03_04_0007_composite_indexes.py Adds composite indexes for common queries.
runtime/ailine_runtime/adapters/db/alembic/versions/2026_02_27_0006_fix_composite_fk_unique_constraints.py Adjusts composite-constraint migration ordering.
runtime/ailine_runtime/adapters/db/alembic/versions/2026_02_12_0001_initial_schema.py Adds unique constraint needed for composite FK behavior.
frontend/src/stores/accessibility-store.ts Adds explicit cleanup helper for media-query listener (testability).
frontend/src/lib/sse-fetch.ts Adds SSE retry/backoff + Last-Event-ID resume support.
frontend/src/lib/api.ts Tightens typing for demo login user payload.
frontend/src/components/tutor/tutor-chat.tsx Adjusts auto-scroll behavior during streaming; tweaks aria attributes.
frontend/src/components/tutor/chat-message-bubble.tsx Memoizes message bubble component to reduce rerenders.
frontend/src/components/setup/setup-wizard.tsx Maps camelCase config to backend snake_case; changes setup apply URL/headers.
frontend/src/components/privacy/privacy-panel.tsx Adds auth headers + abort handling for privacy API calls.
frontend/src/app/[locale]/login/page.tsx Removes auth headers from login request.
docker-compose.yml Adds migrations service and shared anchors; adjusts defaults and dependencies.
agents/ailine_agents/resilience.py Enhances circuit breaker state machine; provides idempotency guard used by SSE.
.env.example Aligns env key names (GOOGLE_API_KEY) and updates defaults (dims/dev/demo).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +42
planner_model: config.plannerModel,
executor_model: config.executorModel,
quality_model: config.qualityModel,
tutor_model: config.tutorModel,
anthropic_api_key: config.llmProvider === 'anthropic' ? config.llmApiKey : '',

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toSetupPayload() doesn’t match the backend SetupConfig schema: the backend expects qg_model, but the payload uses quality_model (will be ignored), so the user-selected quality gate model won’t be applied. Align the field names with runtime/ailine_runtime/api/routers/setup_schemas.py (e.g. use qg_model) and consider removing/avoiding extra fields like llm_model if unused.

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +186
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, delay)
signal?.addEventListener('abort', () => {
clearTimeout(timer)
resolve()
}, { once: true })
})

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each retry delay registers a new abort event listener on the signal, but the listener is only removed if the abort fires. With multiple retries this can accumulate listeners for the lifetime of the signal. Consider registering the abort listener once outside the retry loop, or explicitly removing the listener when the timer resolves normally.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +55
async def subscribe(
self,
event_type: str,
handler: Callable[[dict[str, Any]], Awaitable[None]],
) -> None:
self._handlers[event_type].append(handler)
await self._pubsub.subscribe(event_type)
if self._listener_task is None or self._listener_task.done():
self._listener_task = asyncio.create_task(self._listen())

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisEventBus.subscribe is now async, but the EventBus protocol in domain/ports/events.py defines subscribe as a synchronous method. This makes the interface inconsistent with InMemoryEventBus and will break type-checking and any call sites that treat the bus as an EventBus. Either update the protocol + all implementations/callers to make subscribe async, or keep subscribe sync and internally schedule the async Redis subscription/listener startup.

Copilot uses AI. Check for mistakes.
Comment on lines 185 to 189
jwt_secret = os.getenv("AILINE_JWT_SECRET", "")
jwt_public_key = os.getenv("AILINE_JWT_PUBLIC_KEY", "")
if not jwt_secret and not jwt_public_key:
jwt_private_key = os.getenv("AILINE_JWT_PRIVATE_KEY", "")
if not jwt_secret and not jwt_public_key and not jwt_private_key:
errors.append(

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Production JWT validation now passes if only AILINE_JWT_PUBLIC_KEY is set (no secret/private key), but the auth router requires AILINE_JWT_SECRET or AILINE_JWT_PRIVATE_KEY to sign tokens. Consider validating the signing requirement explicitly (require jwt_secret OR jwt_private_key), and if jwt_private_key is set, also require jwt_public_key for verification.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +136
const res = await fetch(`${API_BASE}/setup/apply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
headers: { 'Content-Type': 'application/json', 'X-Setup-Token': '' },
body: JSON.stringify(toSetupPayload(config)),

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setup apply request always sends an empty X-Setup-Token header. The backend rejects missing/invalid setup tokens (403), so setup will fail by default. The wizard likely needs to prompt for the token (or retrieve it via a secure mechanism) and send the actual value, or omit the header and handle 403 with user guidance.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +45
# Idempotency guard -- prevent duplicate concurrent runs with the same run_id.
_idempotency_guard = IdempotencyGuard(ttl_seconds=300.0, max_size=1000)

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdempotencyGuard is used to prevent duplicate concurrent runs, but its TTL eviction only applies to completed results (_timestamps), not to _in_progress entries. If a pipeline task never reaches complete() (e.g., process crash or a permanently hung run), this key can remain blocked indefinitely and all future requests for the same run_id will return 409. Consider adding TTL handling for in-progress keys (or always releasing in a broader error/timeout path).

Copilot uses AI. Check for mistakes.
Comment on lines +416 to +420
# In-memory LRU fallback cache for JTI blacklist when Redis is down
_REVOCATION_CACHE: dict[str, float] = {}
_REVOCATION_CACHE_MAX = 1000
_REVOCATION_CACHE_TTL = 300.0 # 5 minutes

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this is an “LRU fallback cache”, but entries aren’t promoted on access (the timestamp isn’t updated on cache hit), so eviction is based on insertion/verification time rather than recent use. Either update the comment to reflect actual behavior, or update the cache logic to refresh timestamps on access if true LRU is intended.

Copilot uses AI. Check for mistakes.
Comment on lines 39 to 44
# Also dispatch to local in-process handlers (same-node subscribers)
for handler in self._handlers.get(event_type, []):
try:
await handler(data)
except Exception:
_log.exception("event_handler_failed", event_type=event_type)

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish() both publishes to Redis and directly invokes local handlers. Since subscribe() also starts a PubSub listener that dispatches received messages to the same handlers, in-process subscribers will typically receive the same event twice (once from the direct loop and once from PubSub). Consider removing the direct local dispatch (rely on PubSub delivery) or add a mechanism to suppress self-delivery to avoid duplicate handler execution.

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 44
async def get(
self, run_id: str, *, teacher_id: str | None = None
self, run_id: str, *, teacher_id: str
) -> RunTrace | None:

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing TraceStore.get() to require teacher_id is a breaking API change. There are still call sites/tests that call get(run_id) without teacher_id (e.g. runtime/tests/test_trace_store.py), which will now raise TypeError. Either keep a default (teacher_id: str | None = None) with the old behavior, or update all call sites/tests to pass the authenticated teacher_id.

Copilot uses AI. Check for mistakes.
Comment on lines 43 to 48
_SETUP_TOKEN = secrets.token_urlsafe(32)
import sys
print(
f"\n{'=' * 60}\n"
f" AILINE SETUP TOKEN: {_SETUP_TOKEN}\n"
f" Use this in the X-Setup-Token header for /setup/apply\n"
f"{'=' * 60}\n",
file=sys.stderr,
flush=True,
_log.info(
"setup_token_generated",
msg="Setup token generated. Retrieve via server admin or set AILINE_SETUP_TOKEN env var.",
)
_log.info("setup_token_generated", msg="Setup token printed to stderr")
return _SETUP_TOKEN

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no AILINE_SETUP_TOKEN is set, the server now generates a token but no longer prints or otherwise exposes it. Since require_setup_token() still requires the caller to provide the token, this makes first-run setup impossible unless an env var is preconfigured. Either restore a secure way to retrieve the generated token (e.g., print once to console/stderr, or expose an admin-only retrieval path) and update the error message accordingly.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants