# Runtime Type Detection and Custom Arguments

## Problem

The server constructs Docker container entrypoints for deployed apps, but currently hardcodes a single start command. Different app frameworks require different start commands:

- **Spring Boot**: `-cp app.jar org.springframework.boot.loader.launch.PropertiesLauncher` with `-Dloader.path` for the log appender
- **Quarkus JVM**: `-jar app.jar` (appender is a compiled-in Maven dependency)
- **Plain Java**: `-cp app.jar:/app/cameleer-log-appender.jar <Main-Class>`
- **Native** (Quarkus native): no JVM, just run the binary directly (agent compiled in at build time)

Users also need a way to pass custom command-line arguments (JVM flags, system properties, or native binary args).

## Design

### Runtime Type Enum

New enum `RuntimeType` in the core module:

```
AUTO, SPRING_BOOT, QUARKUS, PLAIN_JAVA, NATIVE
```

### Data Model

Two new keys in the existing `containerConfig` JSONB, participating in the 3-layer merge (app > env > global):

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `runtimeType` | `string` | `"auto"` | One of: `auto`, `spring-boot`, `quarkus`, `plain-java`, `native` |
| `customArgs` | `string` | `""` | Freeform arguments appended to the start command |

No database migration needed for these — they're new keys in existing JSONB columns.

**AppVersion additions** (requires Flyway migration `V10`):

| Column | Type | Description |
|--------|------|-------------|
| `detected_runtime_type` | `VARCHAR` (nullable) | Runtime type detected from JAR on upload |
| `detected_main_class` | `VARCHAR` (nullable) | Main-Class from manifest (needed for `PLAIN_JAVA` entrypoint) |

### Server-Side Runtime Detection

New class `RuntimeDetector` in the core module. Pure function: takes a file path, returns a detection result (runtime type + main class, both nullable).

**Detection logic** (checked in order):

1. **Not a JAR**: file doesn't start with ZIP magic bytes (`PK`) → `NATIVE`
2. **Read `META-INF/MANIFEST.MF`** from the JAR:
   - `Main-Class` contains `org.springframework.boot.loader` → `SPRING_BOOT`
   - `Main-Class` is `io.quarkus.bootstrap.runner.QuarkusEntryPoint` → `QUARKUS`
   - `Main-Class` exists (anything else) → `PLAIN_JAVA` (also extract the Main-Class value)
3. **No Main-Class in manifest** → detection fails, return null for both fields

**When it runs**: immediately after JAR upload in `AppController.uploadJar()`. The result is stored on `AppVersion.detectedRuntimeType` and `AppVersion.detectedMainClass`. The upload API response and version listing include these fields so the UI can show the detection result immediately.

**At deploy time** (PRE_FLIGHT in `DeploymentExecutor`): if `runtimeType` is `AUTO`, use `AppVersion.detectedRuntimeType`. If that is also null, fail the deployment with:

> "Could not detect runtime type for JAR '{filename}'. Set runtimeType explicitly in app configuration."

### Entrypoint Construction

`DockerRuntimeOrchestrator.startContainer()` builds the entrypoint based on the resolved runtime type. `ContainerRequest` gains two new fields: `runtimeType` (String) and `customArgs` (String).

| Type | Entrypoint |
|------|-----------|
| `SPRING_BOOT` | `exec java -javaagent:/app/agent.jar -Dloader.path=/app/cameleer-log-appender.jar {customArgs} -cp {jarPath} org.springframework.boot.loader.launch.PropertiesLauncher` |
| `QUARKUS` | `exec java -javaagent:/app/agent.jar {customArgs} -jar {jarPath}` |
| `PLAIN_JAVA` | `exec java -javaagent:/app/agent.jar -cp {jarPath}:/app/cameleer-log-appender.jar {customArgs} {mainClass}` |
| `NATIVE` | `exec {jarPath} {customArgs}` |

All entrypoints are wrapped in `sh -c "..."` for consistent execution. `jarPath` is resolved per mount strategy (volume mount uses the original path, bind mount uses `/app/app.jar`).

For `PLAIN_JAVA`, `mainClass` comes from `AppVersion.detectedMainClass`. If unavailable (user set `plain-java` explicitly but no main class stored), the deployment fails at PRE_FLIGHT.

All JVM types receive `CAMELEER_AGENT_*` env vars from `buildEnvVars()` — the agent reads them directly.

### Input Validation

`customArgs` is validated on save (in `AppController` and `EnvironmentAdminController`) to prevent shell injection. The entrypoint uses `sh -c`, so shell metacharacters must be rejected.

**Allowed pattern**: `^[-a-zA-Z0-9_.=:/\s+"']*$`

This covers legitimate JVM args (`-Xmx256m`, `-Dfoo=bar`, `-XX:+UseG1GC`, `-Dpath=/some/path`) while blocking shell metacharacters (`;`, `&`, `|`, `` ` ``, `$`, `(`).

Validation error: *"customArgs contains invalid characters. Only JVM-style arguments are allowed."*

`runtimeType` is validated as one of the enum values (case-insensitive).

### UI Changes

The **Resources tab** in the app config gets two new fields at the top (before Memory Limit):

**Runtime Type**: `Select` dropdown with options:
- `Auto (detect from JAR)` (default)
- `Spring Boot`
- `Quarkus`
- `Plain Java`
- `Native`

Below the dropdown, a hint shows the detection result from the current JAR version:
- "Detected: Spring Boot" (green) — when detection succeeded
- "Detection failed — select runtime type manually" (amber) — when detection returned null
- No hint when no version has been uploaded yet

**Custom Arguments**: single-line `Input` text field:
- Placeholder: `-Xmx256m -Dfoo=bar`
- Label: "Custom Arguments"
- Sublabel adapts to runtime type: "Additional JVM arguments" for JVM types, "Arguments passed to the native binary" for `Native`
- Client-side validation feedback on invalid characters

### Files Changed

| Area | Files |
|------|-------|
| New | `RuntimeDetector.java` (core) — detection logic |
| New | `RuntimeType.java` (core) — enum |
| Modified | `ContainerRequest.java` (core) — add `runtimeType`, `customArgs`, `mainClass` fields |
| Modified | `ConfigMerger.java` (core) — resolve `runtimeType`, `customArgs` string fields |
| Modified | `ResolvedContainerConfig.java` (core) — add `runtimeType`, `customArgs` fields |
| Modified | `AppVersion.java` (core) — add `detectedRuntimeType`, `detectedMainClass` fields |
| Modified | `DockerRuntimeOrchestrator.java` (app) — entrypoint construction per runtime type |
| Modified | `DeploymentExecutor.java` (app) — resolve AUTO at PRE_FLIGHT, pass fields to ContainerRequest |
| Modified | `AppController.java` (app) — run detection on upload, validate customArgs on save |
| Modified | `EnvironmentAdminController.java` (app) — validate customArgs on save |
| Modified | `AppService.java` (core) — accept and store detection results |
| Modified | `PostgresAppVersionRepository.java` (app) — persist new columns |
| New | `V10__app_version_runtime_detection.sql` — add columns to `app_versions` |
| Modified | `AppsTab.tsx` (UI) — Runtime Type select + Custom Arguments input + detection hint |
| Modified | `apps.ts` (UI) — add fields to `AppVersion` type |

### Not in Scope

- No changes to the base Docker image — the server constructs the entrypoint fully
- No changes to the JAR upload flow (same endpoint, detection is transparent)
- No environment variable changes — agent reads `CAMELEER_AGENT_*` as-is
- No Quarkus native-specific upload handling — same upload, detected as native binary
