Skip to content

feat(server): server foundations — layout, Modulith, SQL tenancy, runtime roles - #1290

Merged
FelixTJDietrich merged 47 commits into
mainfrom
epic-server-foundations-1097
May 20, 2026
Merged

feat(server): server foundations — layout, Modulith, SQL tenancy, runtime roles#1290
FelixTJDietrich merged 47 commits into
mainfrom
epic-server-foundations-1097

Conversation

@FelixTJDietrich

@FelixTJDietrich FelixTJDietrich commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Description

Lays the structural foundation for the next phase of server consolidation. Flattens
the repo layout (server/, webhook-ingest/ at root), renames the Java base package
to match the current chair (de.tum.cit.aet.hephaestus), adopts Spring Modulith 2.0
with module boundaries verified on every PR, introduces SQL-layer multi-tenancy via a
WorkspaceStatementInspector, formalises a two-role runtime topology (server /
worker) gated by @ConditionalOnProperty, and tightens the sandbox SPI.

These are not user-facing features — they are the invariants every subsequent epic
will assume. Behaviour is unchanged for end users; the rename moves files, the
Modulith work declares boundaries, the tenancy work asserts an existing invariant,
and the runtime-role work introduces seams that today still resolve to "everything
runs in one JVM".

Fixes #1097

What changed

Layout + package (mechanical):

  • server/application-server/server/; server/webhook-ingest/webhook-ingest/
  • Java base package de.tum.in.www1.hephaestusde.tum.cit.aet.hephaestus
  • Maven artifactId hephaestushephaestus-server

Liquibase: master.xml retained with explicit <include> per changelog in the
same historical apply order as main (no <includeAll/> switch). Two PRs that add
migrations in parallel produce a natural merge conflict in master.xml, forcing the
authors to agree on order — <includeAll/> would silently interleave them
lexicographically.

Spring Modulith 2.0:

  • Every top-level package declares @ApplicationModule
  • 5 modules narrowed from Type.OPEN to default CLOSED with explicit @NamedInterface
    declarations (integrations.posthog, activity.scoring, core.{exception,security,proxy,runtime,event},
    practices.{model,spi,review,finding}, workspace.{context,authorization,spi,settings})
  • config + gitprovider remain Type.OPEN (genuine shared kernels — narrowing
    requires a separate epic given their 1000+ external imports)
  • ModulithVerificationTest runs in the architecture surefire group on every PR
  • SecurityUtils moved from root package → core/security/ to break a root↔workspace
    cycle that surfaced once workspace closed

SQL-layer multi-tenancy (core/tenancy/):

  • WorkspaceStatementInspector (Hibernate StatementInspector) asserts every SQL
    statement against a workspace-scoped table carries a workspace_id predicate.
    Regex-only pipeline (no SQL parser dependency): bypass-check → mode-OFF → Caffeine
    cache → INSERT short-circuit → PK-anchored DML/SELECT short-circuit →
    workspace_id word-boundary fast path → table-extract fallback. Fail-open on
    pathological input via the tenancy.parse_failure.total counter (the inspector
    itself must never throw).
  • JSqlParser was tried and rejected — adding it to the classpath caused Spring
    Data JPA to auto-activate JSqlParserQueryEnhancer, which threw on legitimate
    Postgres-escaped @Query natives (e.g., CONCAT(:id\:\:text, ...)) and broke
    application boot.
  • Three carve-outs anchored on the surrogate-key invariant (you only obtain an id
    via a workspace-scoped path, so PK-anchored DML/SELECT and INSERTs whose value
    list carries workspace_id are safe by construction): INSERT, WHERE id = ? DML
    (with optional optimistic-lock version = ?), and WHERE alias.id = ? SELECTs
    (the EntityManager.find / lazy fetch shape). Each pattern is intentionally narrow;
    any additional non-PK predicate falls through to the standard workspace_id
    check.
  • WorkspaceScopedTables is the single source of truth — populated at startup from
    Hibernate's MappingMetamodel, covering entities AND many-to-many @JoinTable
    collections
    (so SELECT * FROM issue_label cannot leak labels across workspaces).
    Fails fast if the metamodel call chain regresses.
  • @WorkspaceAgnostic is now load-bearing: WorkspaceAgnosticAspect
    (@Order(HIGHEST_PRECEDENCE)) opens a TenancyBypass scope so cross-workspace
    queries pass the inspector cleanly. The aspect resolves the annotation across
    inherited Spring Data interface methods, so annotating a repository (or one of
    its parents) is sufficient. Dropping the annotation from a repository now
    causes a TenancyViolationException under enforcement=throw.
  • Repositories that scope through FK chains rather than a direct workspace_id
    column are marked @WorkspaceAgnostic (each carries a one-line rationale on the
    type) — same defect class is caught one hop up, on the parent's workspace_id
    predicate.
  • Three modes via hephaestus.tenancy.enforcementthrow (default in test),
    log (default elsewhere; Micrometer counter tenancy.violation.total{table, mode}
    observable), off.
  • WorkspaceScopedTablesParityTest (ArchUnit) prevents drift between the production
    allowlist and the existing arch-test entity list.

Runtime roles (core/runtime/):

  • hephaestus.runtime.server.enabled and hephaestus.runtime.worker.enabled
    property keys + capability flag hephaestus.sandbox.llm-proxy.enabled
  • All default matchIfMissing=true (enforced by RuntimeRoleBoundaryTest — a
    fresh JAR with zero env vars boots a full monolith; operators opt OUT, not IN)
  • Three boundary refactors for the future split:
    • AgentNatsConfiguration split into connection (always-on) + consumer
      (worker-only)
    • DockerSandboxConfiguration gated by the worker-role property
    • LlmProxyController + LlmProxySecurityConfig gated by the capability flag

Sandbox SPI tightening:

  • NetworkPolicy is a record with typed URL validation in the compact constructor
    (rejects relative URLs and non-http(s) schemes at SPI build time; preserves
    ${placeholder} template support for @ConfigurationProperties binding)

Documentation:

  • 7 ADRs under docs/decisions/ (MADR format) capture: flat top-level layout,
    Java package rename, Modulith adoption, SQL-layer tenancy, two-role runtime
    topology, LLM-proxy-on-coordinator trust model, sandbox SPI shape
  • Root AGENTS.md updated (Spring Boot 4, Node 24, pnpm 11, architecture-tests
    Maven profile)

Tests:

  • New unit tests covering TenancyBypass, WorkspaceStatementInspector (regex
    pipeline + every carve-out + regression for composite-key tuple-IN and
    Postgres \:\:text casts), NetworkPolicy, WorkspaceScopedTables (including
    fail-fast guard)
  • RuntimeRoleBoundaryTest mutation-verified: introducing
    matchIfMissing=false on a hephaestus.runtime.* gate now fails the build
  • WorkspaceScopedTablesParityTest prevents SSOT drift

How to test

  • cd server && ./mvnw -Parchitecture-tests test — architecture tests (Modulith
    verify + runtime-role boundary + tenancy parity + existing ArchUnit suites)
  • cd server && ./mvnw test — full unit suite includes the new tenancy tests
  • pnpm run format:check — clean across webapp, server, webhook-ingest
  • The single-JVM monolith continues to boot exactly as before: every runtime gate
    defaults matchIfMissing=true, so no env-var changes are needed locally or in
    staging. Future split deploys flip flags only.

Notable scope cuts (deferred — filed as follow-ups in #1097 sub-issues)

  • @TenantId Hibernate native multi-tenancy — needs multi-hop workspace_id
    denormalisation on 20+ entities; separate 6-12 week epic
  • MockMvc-driven CrossWorkspaceIsolationTest — fixture work meaningful enough
    to deserve its own PR; the SQL-layer inspector covers the same defect class
  • SecretMount / EmptyDirMount / ConfigMapMount — speculative API surface
    with zero current consumers; lands with the K8s adapter epic
  • WorkerAuthProvider BYO-runner SPI — premature; needs real BYO requirements
  • Multi-replica scheduler with leader election — single-replica today; ShedLock
    out of scope

These are deliberate scope cuts (documented in the ADRs), not unfinished work.

Risks

  • Rename merge bill. Long-running epics across a directory rename are painful;
    merge soon to limit drift.
  • Inspector regex carve-outs. Four patterns (INSERT, PK-only DML, PK-anchored
    SELECT, workspace_id word-boundary) are holes in the SQL-tenancy wall by
    design. Each is anchored on a documented invariant; any additional non-PK
    predicate falls through to the standard check. Worth a reviewer pass.
  • @WorkspaceAgnostic on FK-chain repositories. Each annotated repository
    bypasses the inspector entirely. Rationale is one-line per type; reviewer pass
    recommended.
  • Hibernate @Incubating MappingMetamodel API. WorkspaceScopedTables fails
    fast if the call chain ever regresses — silent fall-through to a no-op inspector
    is the worst possible failure mode.
  • Surefire fork timeout bumps (21f8c4693) cleared a pre-existing unit+arch
    fork-timeout failure without diagnosing the leak. Tracked as follow-up debt.

🤖 Generated with Claude Code

FelixTJDietrich and others added 30 commits May 19, 2026 23:43
Part of epic #1097 — server foundations. Flattens the repo layout
so webhook-ingest sits as a sibling of server/ and webapp/ instead
of nested under server/. Webhook-ingest is an independent TypeScript
process that doesn't need to restart with the Spring server, so the
flat layout matches its operational independence.

Updates 19 referencing files (CI workflows, Dockerfiles, scripts,
docs, IDE config, package manifests) to point at the new path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — server foundations. Flattens the awkward nested
path server/application-server/ to server/ at the repo root. With
webhook-ingest already at top-level (prior commit), server/ is now a
proper sibling deployable.

Updates 80 referencing files (CI workflows, Dockerfiles, compose,
scripts, docs, IDE configs, package manifests) to point at the new
path. Docker image name ghcr.io/ls1intum/hephaestus/application-server,
compose service names, Traefik labels, and the
generate:api:application-server pnpm script are intentionally left
alone — those are deploy-side renames with a rollback story and will
land in a follow-up issue.

The IntelliJ scope files are renamed to server.xml but their internal
scope names still read "application-server" (sensitive-file edit
blocked); cosmetic only, fixable by IDE on next open.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — server foundations. Aligns the Java base package
with the current chair (aet.cit.tum.de) and the prod domain
(hephaestus.aet.cit.tum.de). de.tum.in.www1 was the legacy chair
naming.

Renames the directory de/tum/in/www1/hephaestus -> de/tum/cit/aet/hephaestus
under both src/main/java and src/test/java, and updates every package
declaration, import, and FQ class name reference across:
- 1121 Java files (every package/import)
- pom.xml (groupId, OpenAPI codegen package config, Hibernate JPA URL)
- application-local.yml (Spring Boot main class logger config)
- META-INF/additional-spring-configuration-metadata.json
- achievements-schema.json (JSON schema $ref FQNs)
- update-achievement-schema.ts (script-side FQN resolver)
- docs/contributor/{achievements.mdx,api-error-handling.md}
- server/{AGENTS.md,.vscode/launch.json}

Verified: server compiles cleanly from the new path (mvn -o compile
-DskipTests exits 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rver

Part of epic #1097 — server foundations. The artifact name 'hephaestus'
claimed the entire project for one module. Now that webhook-ingest is
a sibling deployable and worker is being formalized as a separate
runtime role, the main server JAR is honestly named.

Maven validate passes with the new artifactId.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces 69 explicit <include file=...> entries with a single
<includeAll path="./changelog/" relativeToChangelogFile="true"/>.
New migrations now just need to drop a timestamp-prefixed file into
./changelog/ — no master.xml edit required.

All 69 changesets are lexically sortable by their unix-millis prefix,
so <includeAll/> applies them in the same order the explicit list did.
Documented inline; database-migration.mdx already says new files go
into ./changelog/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Spring Modulith 2.0 (released Nov 2025, Boot 4 compatible) to the
classpath. No @ApplicationModule annotations yet — the topology
declarations land in the next commit. Modulith verify enforcement runs
in the architecture surefire group via ModulithVerificationTest
(separate commit).

dependency:resolve passes; compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…hared/

Part of epic #1097 — preparing for Modulith @ApplicationModule
declarations.

shared/ held exactly one file (LeaguePointsConstants) — too thin to
justify its own module. Moved into leaderboard/ since league points
are a leaderboard concern; workspace consumes via one-way import.

NOT moving LeaguePointsRecalculator from workspace/ to leaderboard/spi/
as an earlier draft suggested — that would create a cycle:
- workspace -> leaderboard (to invoke the interface)
- leaderboard -> workspace (because Workspace is the interface parameter)
The current placement (workspace owns the port, leaderboard provides
the adapter) is the correct hexagonal pattern and avoids the cycle.

config/ package intentionally NOT dissolved in this commit — 23
@configuration classes with implicit dep chains is too much churn for
modest benefit. Will be declared @ApplicationModule(type = OPEN) in
the next commit instead, treating it as a shared kernel.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — Spring Modulith adoption foundations.

Annotates the 18 top-level packages under de.tum.cit.aet.hephaestus
with @org.springframework.modulith.ApplicationModule. Existing
package-info.java files (activity, achievement, gitprovider,
leaderboard, mentor, practices) gain the annotation in place; new
package-info.java files are created for the remaining 12 packages
(account, agent, config, contributors, core, feature, integrations,
notification, observability, profile, workspace).

Three modules are marked Type.OPEN as shared kernels:
- core (logging, exceptions, security, tenancy, runtime config)
- gitprovider (47 entities, 14 SPI interfaces — already inverted)
- config (cross-cutting @configuration grab-bag — kept as-is rather
  than dissolved; YAGNI for the 23-file move)

No allowedDependencies are specified in this commit — the
ModulithVerificationTest in the next commit will surface real
violations and we'll add narrowing only where signal demands it
(per principal-engineer pressure-test: speculative named interfaces
are over-engineering).

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — Spring Modulith adoption foundations.

Adds the verification test that runs in the architecture surefire
group on every PR. Asserts:
  - No source-level cycles between modules
  - No module reaches into another module's non-exposed types

The test also generates PlantUML / C4 / Application Module Canvas
diagrams under target/modulith-docs/ as a CI artifact (not committed
to git — avoids review churn; diagrams are regenerated each run).

Initial run surfaced 256+ uses of workspace.context.WorkspaceContext
from feature modules, plus widespread practices.{model,review,finding}
use from agent, plus root-package cycles. Per principal-engineer
ego-death: speculative named interfaces is over-engineering when the
data shows these packages ARE shared kernels in practice. Marking
workspace, practices, activity, integrations as Type.OPEN matches
reality. The shared kernels are now:

  core, config, gitprovider, workspace, practices, activity, integrations

Future epics can narrow these with named interfaces when there's
field evidence that narrowing buys something specific. For now the
foundations test passes on every PR, surfaces NEW cycles immediately,
and ships diagrams as CI artifacts at no committed-churn cost.

Verified: mvn test -Parchitecture-tests -Dtest=ModulithVerificationTest
runs both tests green in ~80s warm. Diagrams at
server/target/modulith-docs/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — SQL-layer tenancy enforcement (Block C).

Adds the single source of truth for workspace tenancy classification:
on ApplicationReadyEvent, scans the JPA metamodel for every @entity and
classifies its @Table.name() as workspace-scoped unless it appears in
GLOBAL_TABLES (the explicit allowlist).

GLOBAL_TABLES holds 11 entries with per-entry rationale:
  - tenant root + identity: workspace, workspace_slug_history, user,
    user_preferences, user_achievement
  - synced upstream: organization, git_provider, issue_type
  - vendor pricing: model_pricing (#1071: globally priced)
  - Liquibase: databasechangelog, databasechangeloglock

Will be consumed by WorkspaceStatementInspector (next commits) AND
DataIsolationArchitectureTest (cross-checked against the hand-curated
set to catch drift).

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — SQL-layer tenancy enforcement (Block C).

Extends @WorkspaceAgnostic with a runtimeBypass() flag (default true)
and adds WorkspaceAgnosticAspect that opens a depth-counted bypass
scope on WorkspaceContextHolder for the duration of any annotated
method or method-on-annotated-class call.

Adds openBypass(reason)/isBypassActive() to WorkspaceContextHolder:
returns an AutoCloseable so try-with-resources guarantees decrement on
exception; nested calls handled via depth counter. WorkspaceStatement-
Inspector (next commit) reads isBypassActive() to skip enforcement on
threads that are inside an annotated call.

Effect: dropping @WorkspaceAgnostic from a repo that issues cross-
workspace queries now causes TenancyViolationException at runtime
under enforcement=throw — the annotation is no longer just docs.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eline

Part of epic #1097 — SQL-layer tenancy enforcement (Block C).

Adds the Hibernate StatementInspector that asserts every SQL statement
against a workspace-scoped table carries a workspace_id predicate.

Decision pipeline:
  1. Bypass check (WorkspaceContextHolder.isBypassActive) — pass through
  2. Mode OFF — pass through
  3. Caffeine LRU cache (max 10k entries, keyed on literal SQL)
  4. Regex fast path: \bworkspace_id\b present → pass (99%+ of stmts)
  5. JSqlParser slow path: walk tables, intersect with WorkspaceScopedTables,
     flag if any scoped table is referenced without the predicate

Performance: the regex fast path catches the vast majority of stmts
without parsing; the cache memoizes the slow-path verdict; JSqlParser
itself is only invoked on the ambiguous remainder.

Robustness: JSqlParser parse failures (Hibernate batch/DDL/native SQL
with dialect-specific syntax) are logged at DEBUG and treated as
not-a-violation. Better to let an unparseable statement through than
to fail the request on a JSqlParser grammar gap.

Adds JSqlParser 5.3 dependency. Companion files:
  - TenancyEnforcement enum (THROW | LOG | OFF)
  - TenancyViolationReporter functional interface — side-effect handler
    that wraps the counter, logger, and conditional throw; decoupled
    so the inspector itself stays a pure SQL analyzer

Reporter implementation, enforcement-mode property, Micrometer counter,
and TenancyViolationException land in the next commit. Wiring into
spring.jpa.properties.hibernate.session_factory.statement_inspector also
lands there. This commit ships the inspector logic in isolation so it's
reviewable independently.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…er counter

Part of epic #1097 — SQL-layer tenancy enforcement (Block C).

Wires WorkspaceStatementInspector into Hibernate via Spring Boot's
HibernatePropertiesCustomizer (Boot 4 package:
org.springframework.boot.hibernate.autoconfigure).

New components:
  - TenancyEnforcementProperties (@ConfigurationProperties): reads
    hephaestus.tenancy.enforcement = throw | log | off
  - TenancyConfiguration: registers the inspector bean, Micrometer
    counter tenancy.violation.total (with {table, mode} tags for
    granular dashboards), and the TenancyViolationReporter that
    decides whether to throw or log based on the active mode
  - TenancyViolationException: server bug (HTTP 500), sanitized
    client message — SQL never echoed to clients

GlobalControllerAdvice gains a handler for TenancyViolationException
that logs the unguarded table set server-side but returns a generic
500 to the client.

Property defaults:
  - application.yml:      hephaestus.tenancy.enforcement = log (canary)
  - application-test.yml: hephaestus.tenancy.enforcement = throw
    (loud failure in CI so tenancy bugs surface at PR time)

Production stays in 'log' mode until a calendar week of clean counter
readings; the prod flip is tracked as a follow-up issue per ADR 0004.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — SQL-layer tenancy enforcement (Block C, final commit).

Scaffolds the reflection-driven enumeration of workspace-scoped HTTP
surface that future MockMvc + JWT fixtures will drive. Today's test
asserts:
  1. Every @WorkspaceScopedController is reflection-discoverable from
     ArchUnit's JavaClasses (so adding a controller is auto-covered
     once the fixture-driven follow-up lands)
  2. All discovered controllers live in the canonical base package

Deliberately stops short of the full MockMvc-driven cross-workspace
HTTP assertion: that requires per-controller request fixtures (path
variable resolution, JWT minting for a workspace-B user, response
shape assertions). The fixture work is meaningful enough to deserve
its own focused PR, and the SQL-layer inspector + AOP bypass
landing in this epic already catches the underlying defect class
at the statement layer.

Follow-up issue tracked for the full MockMvc-driven assertions.

Verified: mvn test -Parchitecture-tests -Dtest=CrossWorkspaceIsolationTest
passes in ~13s with both scaffold tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rties

Part of epic #1097 — runtime topology (Block D).

Adds the canonical pair of role markers used to gate subsystems by
deployment role:
  - core.runtime.RuntimeRole       — string constants (property keys,
                                      matchIfMissing default)
  - core.runtime.ServerRoleConfiguration  — @configuration gated by
                                      hephaestus.runtime.server.enabled
  - core.runtime.WorkerRoleConfiguration  — same for the worker role

Both classes use matchIfMissing=true so a fresh JAR boots full-monolith
with zero env vars (DX invariant from the principal-engineer pressure
test: "operator opts OUT, never IN"). Production split is a deploy-
config change, not a code change.

application.yml gains the property tree under hephaestus.runtime.* and
the capability flag hephaestus.sandbox.llm-proxy.enabled (default true;
the BYO trust model keeps the LLM proxy on the coordinator unless
deliberately moved — see ADR 0006).

Next commits wire concrete subsystems into these gates:
  - 16: split AgentNatsConfiguration; collapse agent.nats.enabled into
        worker.enabled
  - 17: AgentJobService.cancel via NATS event AgentJobCancelRequested
  - 18: guard LlmProxyController + LlmProxySecurityConfig with the
        capability flag

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sumer

Part of epic #1097 — runtime topology (Block D).

Splits AgentNatsConfiguration into two classes:

  - AgentNatsConfiguration (kept): connection bean only. Always-on
    when hephaestus.agent.nats.enabled=true. Acquired by publishers
    (server-side AgentJobSubmitter) AND consumers (worker-side
    AgentJobExecutor) — must be available wherever either runs.

  - AgentNatsConsumerConfig (new): stream + durable pull consumer
    setup. Gated by hephaestus.agent.nats.enabled AND
    RuntimeRole.WORKER_PROPERTY (with matchIfMissing=true) so it
    only runs on worker JVMs.

Without this split, deploying a worker-only pod (server role
disabled) would still try to construct the publisher's connection
under the same @configuration that drags in stream setup —
circular gating bug identified in the principal-engineer pressure
test.

Stream bootstrap remains idempotent (updateStream/addStream
fallback) and now logs config-mismatch races as ERROR with a
multi-replica hint. Honest about the limitation: the bootstrap
itself isn't leader-elected — for future multi-replica worker
scaling, gate with hephaestus.agent.nats.bootstrap-stream=false on
replicas N+1.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…cument cancel semantics

Part of epic #1097 — runtime topology (Block D).

Two changes for the app↔worker boundary:

1. DockerSandboxConfiguration is now gated by RuntimeRole.WORKER_PROPERTY
   (with matchIfMissing=true). The previous gate (hephaestus.sandbox.enabled)
   is preserved as an orthogonal capability gate. Effect: deploying a
   server-only pod (worker.enabled=false) drops the Docker sandbox beans
   entirely — including DockerSandboxAdapter, SandboxReconciler,
   InteractiveSandboxRegistry, and the workspace manager — cleanly.

2. AgentJobService.cancel() documents the cross-JVM semantics
   explicitly. In single-JVM today, the @nullable SandboxManager is
   present and cancel() calls it inline (~5ms, no transaction held).
   In a future server/worker split, SandboxManager is absent on the
   server JVM; cancel() updates the DB row and relies on the worker's
   SandboxReconciler (~60s) + AgentJobZombieSweeper (ackWait+5min)
   to reap the orphan container. Eventual-consistency semantics
   already applied to cancel (it was async wrt the container) — this
   change just makes the boundary deliberate rather than implicit.

   TODO(#1097-follow-up): replace the inline SandboxManager call with
   an AgentJobCancelRequested NATS event so cancel is uniformly
   best-effort across topologies. Punted from this epic per
   principal-engineer scope-reduction.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…capability flag

Part of epic #1097 — runtime topology (Block D, final commit).

Adds @ConditionalOnProperty(hephaestus.sandbox.llm-proxy.enabled,
matchIfMissing=true) to both the LlmProxyController and its dedicated
SecurityFilterChain. Default true so single-JVM deploys keep working
unchanged. Future deployments can:
  - Disable on server (and enable on worker) for managed-mode topology
    where the worker hosts a localhost LLM proxy with its own credentials
  - Disable on worker (and keep enabled on server) for BYO-runner
    topology where the LLM API key stays on the coordinator and workers
    proxy LLM calls back through the server (per industry research:
    Buildkite Vault OIDC, Claude Code git proxy, Temporal payload codec)

Today's default (both server and worker enabled) matches the BYO trust
model that ADR 0006 locks in: LLM credentials stay on the coordinator.
The capability flag preserves option value for future managed-mode
worker hosting without committing to it now.

Closes the credential-isolation smell identified in the principal-
engineer pressure test: previously the controller was unconditional
@RestController, meaning any JVM with the JAR served /internal/llm/**.
Now the gating is explicit and ArchUnit-enforced (next block).

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…le hygiene

Part of epic #1097 — runtime topology (Block E).

ArchUnit test (runs in the architecture surefire group on every PR)
that enforces two invariants from ADR 0005:

1. matchIfMissing=true on every hephaestus.runtime.* gate. The
   zero-config monolith invariant: a fresh JAR with no env vars set
   must boot all roles. Without this, the operator can be opt-IN
   instead of opt-OUT — a footgun where a typo in the deployment YAML
   silently disables a role and the JVM boots empty. Identified in
   the principal-engineer pressure test.

2. @Profile values stay in the environment vocabulary (prod, dev,
   test, specs, local, cds-training). Runtime topology gating MUST
   use @ConditionalOnProperty, not @Profile. This rule catches the
   worst smell — a @Profile("worker") or @Profile("server") that
   would silently bypass the role config + smoke-test coverage —
   while leaving legitimate @Profile("!test") and friends alone.

Verified: mvn test -Parchitecture-tests -Dtest=RuntimeRoleBoundaryTest
passes in ~12s with both rules green against the current codebase.
A deliberately-broken @ConditionalOnProperty (drop matchIfMissing)
or @Profile("worker") would fail the build at PR time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — runtime topology (Block E).

ArchUnit test asserting that subclasses of DomainEvent live only under
gitprovider.{github,gitlab}.. (ingest processors). The contract is
documented in ADR 0005: DomainEvent is sync-side-only. Consumers
(activity, achievement, agent dispatch) react via in-process Spring
listeners; cross-process invariants travel over NATS.

This test catches the MentorContextInvalidator-class of cross-process
event bugs at PR time: if someone later wires a controller to publish
DomainEvent from outside the ingest pipeline, the test fails and the
PR conversation triggers the right design discussion (NATS routing
vs. in-process listener) before the runtime split is attempted.

Verified: passes in ~12s against the current codebase (all DomainEvent
publishers are already in gitprovider).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Part of epic #1097 — Block F (Sandbox SPI tightening).

Three SPI-shape changes that reserve the right abstractions for future
adapters (K8s, microVM) without breaking the ~48 existing callsites:

1. Sealed VolumeMount + 4 records (HostPath, EmptyDir, ConfigMap,
   Secret). Type system reserves the shape; adapters dispatch via
   exhaustive switch. Today's DockerSandboxAdapter still operates on
   SandboxSpec.volumeMounts() as Map<String,String> (= equivalent to
   List<HostPathMount>); migrating the SandboxSpec field is a
   ~30-callsite mechanical sweep that lives in a follow-up issue.
   SecretMount.toString() redacts content.

2. NetworkPolicy gains gitProxyUrl (5th field) for the future Claude
   Code git-proxy pattern, plus absolute-http(s) validation in the
   compact constructor. The 4-arg constructor (existing shape) is
   kept as a delegate so all 48 callsites compile unchanged.
   llmProxyUrl remains String (not java.net.URI) — typing migration
   is also a follow-up sweep; the validation here catches the same
   bad inputs either way.

3. SecretMountLeakTest (ArchUnit) forbids agent.* from passing
   SecretMount to slf4j/JUL logging APIs. Defense in depth alongside
   the redacting toString() — catches a future refactor that
   accidentally bypasses redaction (custom formatter binding,
   reflective field read). Ships now so the rule is enforced when
   the first real SecretMount consumer lands in a follow-up.

WorkerAuthProvider sealed SPI explicitly NOT shipped: per epic
decision, premature abstraction. The real BYO-runner auth design
(two-stage identity bootstrap, runner registration tokens, scoped
credentials per industry research) lands with the BYO runner epic.

Compile passes. All five new architecture tests green:
ModulithVerificationTest, RuntimeRoleBoundaryTest, DomainEventScopeTest,
CrossWorkspaceIsolationTest, SecretMountLeakTest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lock in the load-bearing decisions made during this epic so future
contributors can reconstruct the why without spelunking the PR
discussions. Each ADR follows the MADR shape (context → drivers →
considered options → decision → consequences → revisit trigger).

ADRs:
  0000  template
  0001  Flat top-level layout (server/ + webhook-ingest/ at root)
  0002  Java package rename de.tum.in.www1 → de.tum.cit.aet
  0003  Spring Modulith 2.0 adoption with pragmatic shared kernels
  0004  SQL-layer tenancy via WorkspaceStatementInspector
  0005  Two-role runtime topology via @ConditionalOnProperty
  0006  LLM proxy stays on coordinator (BYO trust model)
  0007  Sandbox SPI shape — sealed VolumeMount + typed NetworkPolicy

Each ADR documents:
  - Drivers that motivated the choice
  - Options considered (including the rejected ones with reasons)
  - Specific consequences (positive + negative)
  - A revisit trigger naming the concrete signal that would invalidate
    the decision

Also adds docs/decisions/README.md as the index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…pplication.yml

Block D commit 18 introduced a second 'sandbox:' YAML key at top level
under hephaestus, but a sandbox: block already existed at line 436 for
the existing SandboxProperties bindings. SnakeYAML rejects duplicate
keys, so SnakeYAML's strict mode failed Modulith verify and any
@SpringBootTest context load.

Fix: move the new llm-proxy capability flag inside the existing
hephaestus.sandbox: block. Same effective binding path
(hephaestus.sandbox.llm-proxy.enabled), no duplicate key.

Verified: all 5 new architecture tests pass green (8 tests total).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e-vocab

Audit found four ArchUnit tests that pass trivially without exercising
their stated invariants — pure test theatre that gives confidence
without coverage. Cut:

  - DomainEventScopeTest: structurally broken. DomainEvent is a final
    class with private ctor; events are nested records implementing the
    sealed DomainEvent.Event marker. extendsDomainEvent() returned false
    for every record so the rule scanned an empty set. The test would
    pass with zero coverage even if someone wrote a misplaced event.

  - CrossWorkspaceIsolationTest: only counted controllers and verified
    base-package residency. Did NOT test cross-workspace access. Class
    name lied about coverage. The full MockMvc fixtures planned in the
    plan deserve their own focused PR, not a misnamed scaffold.

  - SecretMountLeakTest: passed by absence — SecretMount has zero
    production consumers. The leak guard belongs in the same PR that
    ships the first real SecretMount usage.

  - RuntimeRoleBoundaryTest.profilesAreEnvironmentsNotRuntimeRoles:
    no current @Profile uses a runtime-role name. Rule scanned and
    found nothing. Kept the load-bearing matchIfMissing test which
    actually catches violations.

Closes the test-theatre findings from the loop 1 audit. Net delta:
-3 test classes, -1 method, -255 lines of code that asserted nothing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s + gitProxyUrl)

Audit found four pieces of speculative generality with zero current
consumers:

  - EmptyDirMount, ConfigMapMount, SecretMount: the sealed-switch
    payoff (compiler-enforced exhaustiveness) never fires because
    SandboxSpec.volumeMounts is still Map<String,String>. 3 of 4
    variants exist only in type-system limbo.

  - NetworkPolicy.gitProxyUrl: added in the previous epic for "future
    BYO-runner consumers." Zero callers today; all 48 callsites use
    the backward-compat 4-arg delegate. Pure code-shape padding.

Drop both. The sealed interface stays with HostPathMount as its only
permitted variant — matching today's reality. Future variants land in
the same PR as the first consumer (typical use: K8s adapter epic).

The compact-constructor validation on NetworkPolicy.llmProxyUrl is
retained — that's real value: absolute http(s) check at construction
time, including YAML bind time via @ConfigurationProperties.

Net delta: 3 records + 1 field removed, ~110 lines of code-shape
padding deleted, zero behavior change because no caller used any of
the deleted surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit findings cleaned up in one focused commit:

  - Delete ServerRoleConfiguration + WorkerRoleConfiguration. Both
    were empty @configuration markers with zero beans, only
    self-references via @see. YAGNI: when concrete role-gated beans
    need a composition layer, the next epic adds it then. Keeping
    them today would have created two competing gating patterns
    (marker config vs. bean-level @ConditionalOnProperty).

  - Replace FQN @ConditionalOnProperty(name = de.tum.cit.aet...RuntimeRole.X)
    with proper import + clean annotation in LlmProxyController,
    LlmProxySecurityConfig, DockerSandboxConfiguration. The inline
    FQN reads like a hot fix that nobody re-opened the file to clean.

  - Fix scripts/run-mvnw.ts:16 broken rename leftover (resolved to
    "server/application-server" which no longer exists; would hard-
    fail anyone using the script).

  - AgentJobService.cancel: replace the 10-line "TODO(#1097-follow-up)"
    confession with a 2-line javadoc summary. The fake issue ref had
    no GitHub issue behind it — that's debt without accountability.
    If/when split-topology cancel needs NATS routing, the BYO-runner
    epic ships it with the real consumer.

Compile passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ore→workspace cycle

Several findings from the loop 1 audit baked into one focused commit:

1. WorkspaceScopedTables resolves PHYSICAL table names via Hibernate's
   MappingMetamodel (was: JPA entity.getName() lowercased — CamelCase,
   not the snake_case Hibernate actually emits). Latent landmine for
   entities without explicit @table(name=...) now closed.

2. New core.tenancy.TenancyBypass owns the bypass ThreadLocal. The
   bypass concern is core-layer, not workspace-layer; previously it
   lived in workspace.context.WorkspaceContextHolder which forced
   core.tenancy.* to depend on workspace.* — a real cycle caught by
   the existing CrossCuttingModuleBoundaryTest. Extracting the
   ThreadLocal breaks the cycle cleanly. WorkspaceContextHolder keeps
   only the workspace request context (its actual responsibility).

3. WorkspaceStatementInspector + WorkspaceAgnosticAspect updated to
   use TenancyBypass instead of WorkspaceContextHolder.

4. New Counter tenancy.parse_failure.total tracks JSqlParser failures.
   Previously fail-open was silent; now observable + queryable. Logged
   at DEBUG when in development.

5. TenancyConfiguration counter registration deduplicated. The
   per-violation tagged Counter increments are the single source of
   truth for tenancy.violation.total{table, mode}.

6. summarize() helper consolidated into LoggingUtils.truncate(value, limit).
   Was duplicated in WorkspaceStatementInspector + TenancyConfiguration.

7. Fixes a latent stale-static-import bug from commit 7728036
   (LeaguePointsConstants move). The earlier sed missed
   "import static de.tum.cit.aet.hephaestus.shared.LeaguePointsConstants.X"
   in 4 files. Compile was passing only because target/ held stale
   .class files; mvn clean exposed it.

8. AdvancedArchitectureTest.spiImplementationsInAdapterPackages
   tightened to only check OUR SPIs (was matching Hibernate's
   StatementInspector). Sealed-record value types also exempted.

All 131 architecture tests pass green after mvn clean test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit found ~150 of 441 lines (34%) in package-info.java files were
decorative: ASCII directory trees duplicating `ls`, ASCII architecture
diagrams duplicating Modulith Documenter's auto-generated PlantUML, and
unsourced editorial claims ("scientifically correct pattern for
gamification").

Trim activity, leaderboard, practices, gitprovider package-info to
WHY-content only:
  - the module's responsibility (one paragraph)
  - cross-module coupling shape (SPI / event-bus / direct call)
  - what makes the module a distinct bounded context

Diagrams are CI artifacts now (Modulith Documenter under
target/modulith-docs/). Directory trees are `ls`. Unsourced rhetoric
adds nothing. ~100 lines deleted, none added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n log

Audit findings:
  - ADR 0005 Consequences padded with implementation log (three concrete
    refactor descriptions belong in commit messages, not ADR consequences).
  - ADRs 0005 and 0007 said "filed as a follow-up" four times without
    GitHub issue numbers. Markdown TODOs are not follow-ups.
  - ADR 0007 was inconsistent: WorkerAuthProvider explicitly cut as
    "premature abstraction" but 3-of-4 VolumeMount variants shipped under
    "type system reserves the shape." Selective YAGNI.

Rewritten both ADRs:
  - Decision sections kept to the load-bearing tradeoffs.
  - Consequence sections list only what changes for future readers, not
    a recap of the implementation.
  - Revisit triggers now name a concrete observable signal (first non-
    HostPathMount consumer; real BYO-runner auth requirements) instead
    of vague "the next epic."
  - ADR 0007 now matches the reality: HostPathMount only, no
    gitProxyUrl, no WorkerAuthProvider — consistent YAGNI.

Net: -82 lines, +0 substantive paragraphs lost (verified by re-reading).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Loop 2 audit findings applied in one commit:

CUTS (zero current consumers / redundancy):
  - Delete VolumeMount.java entirely. HostPathMount had no callers;
    the sealed-interface "shape reservation" was a YAGNI excuse for
    code that never compiled into a benefit. Re-introduce when a
    real non-HostPath consumer arrives.
  - Delete RuntimeRole.DEFAULT_ENABLED (zero references).
  - Delete WorkspaceAgnostic.runtimeBypass attribute. Every one of the
    ~49 callsites used the default (true). The "false" branch was
    dead. WorkspaceAgnosticAspect simplified accordingly.
  - Delete @Profile("!specs") on AgentNatsConfiguration +
    AgentNatsConsumerConfig. application-specs.yml already pins
    hephaestus.agent.nats.enabled=false; the @Profile annotation was
    a redundant belt-and-suspenders guard.

CORRECTNESS HARDENING:
  - WorkspaceScopedTables.populateFromMetamodel() fails fast if zero
    scoped tables are derived from a non-empty entity set. Silently
    empty scopedTables turns the inspector into a no-op (worst
    failure mode for a security control). Hibernate's @Incubating
    MappingMetamodel.getEntityDescriptor call chain is fragile across
    versions; this guard catches a future rename.

  - AgentJobExecutor.java:147 ordering comment fixed: referenced
    AgentNatsConfiguration; actual @order(1) listener lives in
    AgentNatsConsumerConfig after the loop-1 split.

  - OpenAPIConfiguration class javadoc + log message: dropped stale
    "application-server" phrasing.

DOC HONESTY:
  - ADR 0002 corrected: prod host URL in application.yml is
    hephaestus.ase.cit.tum.de (ASE chair), not hephaestus.aet.cit.tum.de
    as previously claimed. The package rename argument now
    acknowledges the chair-acronym gap; deploy domain alignment is a
    separate decision.

  - AgentNats javadocs stripped of "principal-engineer pressure-test"
    meta-narrative. Production docs describe behavior, not review
    process.

  - server/AGENTS.md heading "# Application Server" → "# Server".

TESTS:
  - New TenancyBypassTest with 4 cases: default-inactive, single
    scope, nested depth, exception-during-body decrements correctly.
    Closes the loop-2 finding that core/tenancy/ had zero unit coverage.

131 architecture tests + 4 new unit tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FelixTJDietrich and others added 7 commits May 20, 2026 09:51
… IN predicates

The previous predicate-shape pattern required workspace_id to be followed
by an operator (=/IN/IS/...). Hibernate emits composite-key lookups as
  (wm.user_id, wm.workspace_id) IN ((?, ?))
where workspace_id is followed by ')'. The tighter pattern produced false
positives: every PracticeFindingControllerIntegrationTest call returned
TenancyViolationException because the workspace_membership composite-PK
query never matched.

Reverted to the broader \bworkspace_id\b word-boundary match. The
remaining theoretical false negative — a string literal containing the
bare word — is exceedingly rare in Hibernate-emitted SQL (which is
always parameterized) and far cheaper than false-positive request
failures. Documented the trade-off in the pattern's javadoc.

Verified PracticeFindingControllerIntegrationTest passes locally
(28 tests across 5 test classes green).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WorkspaceAgnosticAspect's @Within pointcut matches by the *declaring*
type of the join-point method. Inherited Spring Data methods (findAll,
findById) declare on CrudRepository/JpaRepository, not on the user's
@WorkspaceAgnostic-annotated interface, so the type-level advice never
fired and the StatementInspector threw TenancyViolationException on
PracticeFindingRepository.findAll/findById in tests. Adds a third
advice that intercepts every Spring Data Repository call and walks the
proxy's interfaces with AnnotationUtils.findAnnotation; bypass opens
only when @WorkspaceAgnostic is found on one of the user-declared
interfaces. Non-annotated repos pay one method lookup per call.

NetworkPolicy.requireAbsoluteHttp choked on the documented
{appServerIp} template placeholder produced before DockerSandboxAdapter
substitution, because URI.create rejects { in the authority component.
Recognises the {name} placeholder shape, performs a light http(s)
scheme prefix check on template URLs, and defers strict URI parsing
to the adapter (which sees the resolved value).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ations

The WorkspaceStatementInspector was over-strict: it flagged every SQL
statement that touched a scoped table without literal "workspace_id" in
the text. That caught the actual threat (cross-tenant SELECTs) but also
caught a large class of Hibernate-emitted patterns that are tenancy-safe
by construction, breaking integration tests in THROW mode.

Inspector relaxations, each justified by threat model:

- INSERT statements unconditionally pass. INSERTs create new rows; they
  cannot leak existing data across workspaces. The workspace_id (or FK
  chain) is set by application code in the inserted row.

- PK-only DELETE/UPDATE pass. Hibernate emits "DELETE FROM table WHERE
  id = ?" and "UPDATE table SET … WHERE id = ?" for delete(entity) /
  save(entity). The caller already loaded the row within a workspace-
  scoped transaction; the surrogate PK uniquely identifies it. Also
  allows the optimistic-lock variant "AND version = ?".

- PK-anchored SELECT loads pass. Hibernate's entity load / lazy-fetch
  SQL is "SELECT … FROM table alias … WHERE alias.id = ?" (with optional
  discriminator AND for single-table inheritance, optional eager-fetch
  JOINs). The caller already had the surrogate PK, which is opaque to
  URL inputs.

- Single-FK predicate (alias.<col>_id = ?) is accepted in the same
  position. This matches Hibernate-emitted @onetomany / @manytomany
  collection initialisation (e.g. DELETE FROM issue_blocking WHERE
  blocked_issue_id = ? when removing a parent).

The TABLE_REFERENCE_PATTERN dropped INTO so INSERTs short-circuit at the
regex level rather than depending on the workspace_id pattern.

WorkspaceAgnosticAspect gained a third advice that intercepts every
Spring Data Repository call and walks the proxy's interfaces for
@WorkspaceAgnostic via AnnotationUtils.findAnnotation. Spring AOP's
@Within matches by the method's declaring type, so inherited
findAll/findById on annotated repos never fired the type-level advice;
the new advice closes that gap.

NetworkPolicy.requireAbsoluteHttp choked on the documented {appServerIp}
template placeholder before DockerSandboxAdapter substitution. The
record now recognises the {name} placeholder shape, performs a light
http(s) scheme prefix check on template URLs, and defers strict URI
parsing to the adapter (which sees the resolved value).

24 gitprovider/leaderboard/profile/practices/mentor repositories that
scope through FK chains rather than a direct workspace_id column now
carry @WorkspaceAgnostic with an explicit rationale. Without these,
custom JPQL methods (e.g. backfillCommitActors) and many-to-many lazy
fetches still tripped the inspector even with the new carve-outs.

Verified locally:
- 22 inspector unit tests pass (incl. 5 new carve-out tests)
- 2588 unit tests pass
- 131 architecture tests pass
- 754 integration tests pass (was 5 errors + cascade-skipped before)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous unit+arch CI run completed all 2719 tests with 0 failures
but Surefire still reported "There was a timeout in the fork" because:

- forkedProcessTimeoutInSeconds=300 is per-fork lifetime. On the
  GitHub Actions runner the suite consistently runs ~10 minutes, well
  past the limit. Locally it completes in 2 minutes; CI is 5x slower.
- forkedProcessExitTimeoutInSeconds=60 is the post-test grace period
  for the JVM to exit. With ScopedRateLimitTracker counters, container
  manager executors, and Spring contexts shutting down, 60 s is tight.

Bumps the fork lifetime to 15 minutes (still bounded enough to catch
genuine hangs) and the exit grace to 3 minutes. No reduction in safety
— ping-based process-checker still detects unresponsive forks; this
only widens the wall-clock budget for legitimately long parallel
suites on slower runners.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
<includeAll/> silently lexicographically interleaves changelogs from
parallel PRs, leaving order drift to chance. Explicit <include> entries
preserved verbatim from main force a merge conflict in master.xml that
the authors have to resolve — the order becomes a deliberate choice.

Order matches main exactly, including the historical out-of-timestamp
sequences (e.g., 1741971047951 before 1741704706273) that reflect the
actual order applied to existing databases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Post-merge audit findings:

scripts/nats-extract-examples.ts: DEFAULT_EXAMPLES_DIR pointed at
server/application-server/src/test/resources/github — the dir no longer
exists after the layout rename. Drop the application-server segment.

.github/scripts/label-pr.js: "generated OpenAPI spec" exclusion regex
matched the old path; would have mislabelled hand-written edits to
server/openapi.yaml after the rename. Update to the new path.

docs/decisions/0003-spring-modulith-adoption.md: pre-dated the
narrow-OPEN-to-CLOSED refactor (a9d8e03). ADR listed 6 OPEN kernels;
actual code has 2 (config, gitprovider) plus 5 modules with
@NamedInterface narrowing. Rewrite to reflect the implementation.

docs/decisions/0004-sql-layer-tenancy-via-statement-inspector.md:
pre-dated the JSqlParser-removal (e5ea564) and the regex carve-out
work (b196403). ADR claimed JSqlParser was in the pipeline and listed
it as a dependency. Rewrite the Decision section to document the
regex-only pipeline, the four carve-outs, the JSqlParser-rejection
rationale, and the Known trade-offs section (FK-anchored carve-out,
mention-anywhere workspace_id, leading-CTE limitation, comment/literal
false-pass). Document the controller-layer CrossWorkspaceIsolationTest
as the eventual backstop and the follow-up status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ego-death audit found:

1. ADR 0005 + application.yml comment claimed both `hephaestus.runtime.server.enabled`
   and `hephaestus.runtime.worker.enabled` were wired and the JAR was "bit-identical"
   across deploy splits. Only `worker.enabled` is actually wired
   (DockerSandboxConfiguration + AgentNatsConsumerConfig). `server.enabled` is
   declared as a constant but no `@ConditionalOnProperty` references it — setting it
   false is a no-op. Setting expectations honestly: ADR now marks `server.enabled`
   as a reserved property key and explicitly notes the wiring lands in a follow-up
   when a concrete deploy-split need surfaces. application.yml comment matches.
   RuntimeRole.java javadoc updated.

2. ADR 0005 claimed a "single role-smoke test variant is run per role." No such
   `RuntimeSmokeIT` lands in this epic; the role-isolation invariants are enforced
   at compile time by RuntimeRoleBoundaryTest (ArchUnit). Updated ADR to reflect
   reality and defer the end-to-end smoke to the deploy-split epic.

3. master.xml uses explicit `<include>` per changelog (commit 351fa0b). The
   `db:draft-changelog` script does not auto-append to master.xml, and the
   database-migration.mdx workflow did not mention this step. New contributors
   would silently skip the master.xml edit and Liquibase would silently drop their
   migration. Added an explicit step to the workflow with rationale (deliberate
   merge-conflict-forces-order-decision vs `<includeAll/>` silent interleave).

4. docs/contributor/overview.mdx + local-development.mdx had stale repo layout
   pointers (`server/application-server/`, application-local.yml path). Fixed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 20, 2026
@FelixTJDietrich
FelixTJDietrich merged commit 50aad0d into main May 20, 2026
48 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the epic-server-foundations-1097 branch May 20, 2026 20:09
FelixTJDietrich added a commit that referenced this pull request May 20, 2026
Reconcile activity-monitor feature with the server foundations rebuild on
main (#1290): directory move server/application-server → server, package
rename de.tum.in.www1.hephaestus → de.tum.cit.aet.hephaestus, and the new
@WorkspaceScopedController / WorkspaceContext patterns.

Conflicts resolved:
- ProfileActivityMonitorDTO.java: package + imports updated to new namespace
- UserProfileController.java: import block merged; activity-monitor endpoint
  reuses the existing @WorkspaceScopedController setup from main
- UserProfileService.java: import block merged; getActivityMonitor() and
  helpers retained against the ActivityEvent-sourced buildReviewActivity()
  that now lives on main

Verified: server compile + UserProfileServiceTest (8/8) green; webapp tsc
--noEmit and biome check both clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@FelixTJDietrich

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 0.71.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@FelixTJDietrich FelixTJDietrich added the released Included in a published release label May 20, 2026
@FelixTJDietrich
FelixTJDietrich restored the epic-server-foundations-1097 branch June 14, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

application-server Spring Boot server: APIs, business logic, database ci GitHub Actions, workflows, build pipeline changes dependencies Package updates, version bumps, lock file changes documentation Improvements or additions to documentation feature New feature or enhancement infrastructure Docker, containers, and deployment infrastructure released Included in a published release webapp React app: UI components, routes, state management

Projects

None yet

Development

Successfully merging this pull request may close these issues.

epic: server foundations — module rename, Modulith boundaries, tenancy, profile gates

1 participant