English | 한국어
A dynamic API-server library that uses Spring Boot as a runtime platform. While the server is running it takes Java source → compiles it → loads it under a dedicated ClassLoader → registers it as REST endpoints → and hot-swaps, rolls back, and unloads it with zero downtime. You can attach and detach APIs without a restart or redeploy.
Two control surfaces — REST and MCP (Model Context Protocol) — deploy and
debug modules. Over MCP an AI agent connects directly (HTTP/stdio) and runs the
whole development loop against a running server, no restart: author source → deploy
behind the test/bytecode/verify gates → debug live (JDI breakpoints, evaluate,
hot-swap) → observe traces & metrics → roll back. This surface is powerful (RCE-class),
so authenticate it outside local use — see 08. MCP Integration.
Coordinate: org.htcom:protean (Spring Boot 3.5.x / Java 21).
Compatibility: Protean currently targets Spring Boot 3.5.x on Java 21. It runs inside your app and manipulates Spring MVC/context internals, so the supported line is a contract — a consumer on Spring Boot 4.x is not yet supported (you may hit runtime linkage errors). 4.x support is a committed, planned track — see the roadmap.
- Dynamic loading engine — compile Java source at runtime → load under a dedicated ClassLoader → register REST mappings, with zero-downtime hot-swap/rollback/unload. No restart or redeploy. On unload it even purges Spring/ClassLoader cache leaks.
- Promotion pipeline (trust model) —
installmust pass ①tests ②bytecode review ③live verify before serving. Ed25519 signing and human approval are opt-in. Premise: "all source = trusted developer". - Versioning & rollback — every deploy accrues version history; roll back to a specific
version (
/rollback?version=). A canary update that fails ③verify auto-rolls back. - Restart recovery — descriptors are persisted to a write-ahead store (filesystem or
JDBC — vendor-adaptive H2/MySQL/PostgreSQL, pluggable); on restart
reconcilerestores the ACTIVE modules. No reliance on in-memory state. - Three isolation modes —
in-process/worker(separate JVM) /container(Docker · cgroup · seccomp · read-only FS). Chosen per module; all support zero-downtime hot-swap, worker pooling, crash supervision, and a dedicated DB. - RPC bridge — a worker calls the main process's shared beans (dynamic proxy over
arbitrary interfaces). Opt-in shared-secret auth (token/HMAC) and
InputStreamreturn streaming. - Runtime execution guard — a request-timeout watchdog (
protean.module.request-timeout-ms) cuts off runaway/blocking handlers (503 on exceed). Cooperative interrupt — only blocking calls are released. - MCP adapter — an MCP agent deploys, queries, and debugs modules over HTTP/stdio. Zero-dep JSON-RPC, open-core (consumers can contribute custom tool beans).
- Level 3 debugging — JDI-based breakpoints, stepping, variable inspection, expression evaluation, fix-and-continue (redefine). Zero-dep.
- Module data-access contract — provides only engine-agnostic mechanism (resource channel, multi-DataSource, per-scope DB auto-provisioning + tenant scoping); ORM, pooling, and policy are left to the consumer.
- Structured errors (RFC 9457) — MCP/HTTP/Admin errors surface as RFC 9457
problem + extension members correlated by
traceId, so an agent self-corrects and retries deterministically instead of parsing prose. - Runtime observability — request traces (module attribution +
traceIdlog correlation) plus opt-in per-module aggregate metrics (request count, error rate, latency p50/p95/p99). Queryable via REST (/platform/traces) and MCP.
See Core concepts for how each works and Control surface for how to drive them. Axes that need to vary are opened via bean registration (open-core) — see Extension points (SPI).
The unit of deployment. Declared as a ModuleDescriptor (record) — id/version,
controller/component FQCNs, sources (FQCN → Java source), tests (promotion
gate input), resources (path → non-Java resource, e.g. mapper XML), isolation
mode, verification plan, signature, etc. It can also be declared with a
module.yaml manifest.
A module must pass the promotion pipeline at install before it serves. Each
stage toggles via protean.gate.*:
(signature opt-in) → ①tests → ②review(bytecode) → (approval opt-in) → ③verify(live) → ACTIVE
- ① tests — the module's bundled JUnit tests are compiled and run at runtime and must pass (no tests = rejected).
- ② review — an ASM bytecode static scan (
ForbiddenApiRuleblocksSystem.exit/Runtime.exec/Runtime.addShutdownHook, …). Register aCodeRulebean to add rules (extension point). - ③ verify — HTTP probe / concurrency / timeout / memory checks on a real port. Auto-rollback on failure.
- signature gate (opt-in) — Ed25519 signature verification (trust store).
- approval gate (opt-in) — auto-gate-passing modules are held in
PENDING_APPROVAL; a human mustapprovethem to reach ③verify + deploy →ACTIVE. An unapproved module is not served even after a restart (no bypass).
DesiredState = { ACTIVE, INACTIVE, PENDING_APPROVAL }. Only ACTIVE is
reconciled at startup.
Each module can pick an isolation mode (ModuleDescriptor.isolationMode,
null = the global default protean.isolation.mode):
| Mode | Implementation | Isolation level |
|---|---|---|
in-process |
InProcessIsolation |
Same JVM · dedicated ClassLoader + child ApplicationContext (default) |
worker |
WorkerProcessIsolation |
Separate JVM process · reverse-proxy forwarding |
container |
ContainerWorkerIsolation |
Docker container · cgroup memory/PID · read-only FS · cap-drop · seccomp |
Every mode supports zero-downtime hot-swap, pooling, supervision (crash restart),
and a dedicated DB. When needed a worker calls the main process's shared beans
over an RPC bridge (declared via bridgedInterfaces).
All source = trusted developer, by design (the in-process production case). A
security sandbox for untrusted source is an explicit non-goal
(SandboxAbsenceTest proves the absence).
Protean does not pick a data-access engine — it provides mechanism and leaves
policy to the consumer. A module configures its own persistence layer
(JdbcTemplate / MyBatis / JPA / multi-DataSource) with arbitrary @Configuration
inside its child context.
- Drivers/ORM = host-bundled. A module is source-only; drivers/ORM go on the host
and the module uses them parent-first. Drop a jar into
protean.module.shared-lib-dir(app-lifetime CL, noDriverManagerleak). The bundled mysql/postgres drivers are optional in the published POM (not forced transitively). - Resource channel —
ModuleDescriptor.resources(path → content, binaries base64) ships non-Java files such as mapper XML,persistence.xml, migration SQL; the module ClassLoader serves them owned-child-first. - Per-scope DB provisioning —
protean.worker.db.auto-provisionturns deployment into "select a scope" (tenant/business-domain): each scope gets a dedicated DB/schema + user, and same-scope modules share it and pack into that scope's worker/container. Operator-driven scope lifecycle (create/close/detach/destroy) via/platform/scopes+ MCPscope_*; MySQL/ Postgres built in, vendor extension via aDbDialectbean. - Transactions — follow from the DataSource choice: in-process + shared DataSource + parent tx manager participates in the host tx; a private DataSource is isolated; worker/container are always isolated (crossing is via the RPC bridge).
- Managed execution & unload —
ProteanTaskExecutor(per-module, lazy, bounded; auto-shuts-down on unload) + theModuleUnloadCallbackSPI. - update diff — a resource-only update skips recompilation while keeping the zero-downtime swap.
Pool sizing, sharding, routing, multi-tenancy, XA, and ORM choice are consumer policy (out of scope).
Protean is published to Maven Central as a plain jar (no Spring Boot fat jar), so
mavenCentral() is the only repository a consumer needs.
dependencies {
implementation 'org.htcom:protean:0.0.1'
// The consumer app is a Spring Boot app so it already has this
// (Protean transits spring only at runtime scope):
implementation 'org.springframework.boot:spring-boot-starter-web'
}Auto-configuration (ProteanAutoConfiguration) loads automatically. The admin
REST (/platform/modules) is on by default; MCP and debugging are fail-safe off.
Deploying a module (REST):
curl -X POST localhost:8080/platform/modules \
-H 'Content-Type: application/json' \
-d '{ "id": "hello", "version": "1", "controllerFqcn": "gen.HelloController",
"sources": { "gen.HelloController": "...java source..." },
"tests": { "gen.HelloTest": "...junit source..." } }'| Method | Path | Action |
|---|---|---|
GET |
/platform/modules |
List ACTIVE modules |
GET |
/platform/modules/{id} |
Single status |
POST |
/platform/modules |
Deploy (201 on gate/verify pass) |
POST |
/platform/modules/from-manifest |
Deploy from module.yaml |
PUT |
/platform/modules/{id} |
Canary update (hot-swap) |
PATCH |
/platform/modules/{id} |
Delta/patch update (file overlay) |
POST |
/platform/modules/{id}/reload-resources |
Swap resources, no downtime (no recompile) |
GET |
/platform/modules/{id}/versions |
Version history |
GET |
/platform/modules/{id}/routes |
Live-registered routes (ground truth for ACTIVE) |
POST |
/platform/modules/{id}/rollback?version= |
Explicit rollback |
POST |
/platform/modules/{id}/approve?approver= |
Approve → ACTIVE |
POST |
/platform/modules/{id}/reject?approver= |
Reject |
DELETE |
/platform/modules/{id} |
Unload |
| Method | Path | Action |
|---|---|---|
GET |
/platform/traces |
Request traces (newest-first). Filters moduleId·errorsOnly·status·minLatencyMs·since·beforeSeq·limit |
GET |
/platform/traces/metrics |
Per-module aggregate metrics (count, error rate, latency p50/p95/p99). Requires protean.trace.metrics.enabled |
GET |
/platform/traces/stream |
Live SSE push (ready ack, then trace/metrics/modules/summary events) |
| Method | Path | Action |
|---|---|---|
GET |
/platform/config |
All keys with current value + tier |
GET |
/platform/config/{key} |
Single key |
PATCH |
/platform/config |
Apply a {key: value} patch (atomic; aborts on any unknown/invalid key) |
| Method | Path | Action |
|---|---|---|
GET |
/platform/shared-libs |
Current generation + stored libs |
GET |
/platform/shared-libs/{name} |
Single stored-lib metadata |
POST |
/platform/shared-libs |
Upload a jar bundle as a new generation (multipart) |
DELETE |
/platform/shared-libs/{name} |
Remove from store (future generations only) |
Streamable HTTP (POST /platform/mcp) + stdio (protean.mcp.stdio). Zero-dep JSON-RPC
(no SDK). Consumers that register a custom McpTool bean have it exposed alongside
(open-core). The library does not implement auth — it delegates to the consumer's Spring
Security + the ModuleActionAuthorizer SPI. Built-in tools (all under protean.*):
| Group | Tool | Action |
|---|---|---|
| Lifecycle | deploy_module |
Deploy (ACTIVE on passing gates ①②③) |
update_module |
Canary update (hot-swap, auto-rollback on failure) | |
patch_module |
Delta update (file overlay, changed files only) | |
rollback_module |
Roll back to a specific version | |
approve_module / reject_module |
Approval gate: approve→ACTIVE / reject | |
uninstall_module |
Unload | |
reload_module_resources |
Swap resources with no downtime (no recompile) | |
| Query | get_module / list_modules |
Single status / list |
get_module_source |
Fetch module source | |
module_versions |
Version history | |
| Observability | query_traces |
Request traces (filters, paging) |
module_metrics |
Per-module aggregate metrics |
JDI-based (jdk.jdi, zero-dep) interactive step debugging. evaluate supports the full
expression grammar (operators, casts, new, lambdas, method references). Tools (all under
debug.*):
| Group | Tool | Action |
|---|---|---|
| Session | launch |
Deploy to a JDWP-enabled worker + auto-attach (reverts to normal deploy on end) |
attach |
Attach to an already-running JDWP JVM | |
list_sessions |
List open debug sessions (reattach) | |
terminate |
End the session (tears down a launch worker) | |
| Execution | set_breakpoint |
Breakpoint at className:line |
continue / step |
Resume / over·into·out step | |
await_stop |
Wait for the next stop (breakpoint / step completion) | |
| Inspection | frames |
Stack frames of the suspended thread |
get_variables |
Frame locals (needs -g compilation) |
|
evaluate |
Evaluate an expression in the suspended frame | |
| Hot-swap | redefine |
Replace a method body in place (fix-and-continue) |
org.htcom.protean
├── ProteanApplication app entry point (auto-config stands in when consumed as a library)
├── autoconfigure/ ProteanAutoConfiguration · ProteanProperties (protean.* config)
├── compiler/ JSR-199 in-memory compile (RuntimeCompiler) · ModuleClassLoader · ModuleSharedLibs
├── dynamic/ dynamic RequestMapping register/deregister (DynamicEndpointRegistrar)
├── proxy/ ReverseProxy (worker routing)
├── module/ ModulePlatform (facade) · ModuleDescriptor · ModuleResource · Store (FS/JDBC) · Reconciler
│ · ProteanTaskExecutor · ModuleUnloadCallback (unload SPI)
├── gate/ promotion gates ①②③ · signature · rules/ (CodeRule SPI)
├── isolation/ IsolationStrategy SPI + InProcess/Worker/Container + WorkerRuntimeProvider
├── worker/ worker management (admin · port handshake)
├── bridge/ worker→main RPC bridge (dynamic proxy over arbitrary interfaces)
├── db/ DB-scope auto-provisioning · DbDialect SPI (built-in MySQL/Postgres)
├── runtime/ execution-timeout watchdog · correlation id · request trace + per-module metrics (TraceStore · TraceMetrics)
├── error/ structured errors (RFC 9457): ErrorCode · ProblemDetail · ProteanException
├── web/ control-plane REST (ModuleAdminController · TraceAdminController)
├── mcp/ MCP adapter (dispatcher · tools/ · transport/ · debug/)
└── boot/ worker/stdio launchers (ProteanWorkerLauncher · ProteanMcpStdioLauncher)
The core facade is module/ModulePlatform —
install/update/rollback/approve/reject/uninstall/reconcile all delegate here, and
gate + verify + isolation routing happen inside it.
A library consumer can't extend the code at their point of use, so Protean opens the axes that need to vary via bean registration:
| SPI | How to register | Purpose |
|---|---|---|
gate.rules.CodeRule |
register a CodeRule bean |
Add a custom bytecode rule to promotion gate ② |
db.DbDialect |
register a DbDialect bean |
Add/override a vendor for DB-scope provisioning (built-in mysql/postgresql fallback) |
module.ModuleStoreDialect |
register a ModuleStoreDialect bean |
Add/override a vendor for the JDBC module-store backend (built-in h2/mysql/postgresql) |
mcp.ModuleActionAuthorizer |
register a ModuleActionAuthorizer bean |
Authorize MCP module actions (default permissive) |
isolation.WorkerRuntimeProvider |
register a bean | Swap the worker deployment model (embed/sidecar) |
module.ModuleUnloadCallback |
register a module/consumer bean | Cleanup hook for out-of-context resources (ThreadLocal, MBean, …) on unload |
ModuleDescriptor.bridgedInterfaces |
descriptor declaration | Main shared interfaces a worker calls over RPC |
Injected into module code: module.ProteanTaskExecutor (per-module, lazy, bounded
managed executor; auto-shuts-down on unload).
Example — adding a custom DB dialect:
@Bean
DbDialect oracleDialect() {
return new DbDialect() {
public String id() { return "oracle"; }
// ... createScope / dropScope / scopedUrl / maxNameLength
};
}
// config: protean.worker.db.dialect=oracleProteanProperties is the typed config surface (with IDE-completion metadata).
Excerpt:
| Key | Default | Description |
|---|---|---|
protean.isolation.mode |
in-process |
Global isolation mode: in-process|worker|container |
protean.admin.enabled |
true |
Expose the admin REST |
protean.mcp.enabled |
false |
MCP server (a deployment entry, an RCE surface → fail-safe off) |
protean.mcp.stdio / .debug.enabled |
false |
stdio transport / debug tools |
protean.gate.tests-enabled / .review-enabled |
true |
Promotion gates ①/② |
protean.gate.signature.required / .approval.required |
false |
Signature / approval gate |
protean.module.request-timeout-ms |
0 |
Module request timeout (0 = unlimited) |
protean.module.shared-lib-dir |
(empty) | Shared lib dir — its jars go on the module CL parent + compile classpath (drop-in) |
protean.module.executor.pool-size |
2 |
Managed executor (ProteanTaskExecutor) pool size |
protean.module-store.backend / .dir / .dialect |
filesystem |
Descriptor store: filesystem|jdbc (jdbc dialect auto-detected; h2/mysql/postgresql built in) |
protean.trace.enabled / .capacity |
true / 200 |
Request-trace ring buffer |
protean.trace.metrics.enabled |
false |
Per-module aggregate metrics (count, error rate, latency percentiles; opt-in) |
protean.trace.metrics.latency-buckets / .max-modules |
20 / 512 |
Latency histogram buckets / tracked-module cap |
protean.worker.modules-per-worker / .min-warm |
4 / 0 |
Worker packing / warming |
protean.worker.auto-restart / .rpc-bridge |
false |
Crash restart / RPC bridge |
protean.worker.runtime |
embed |
Worker runtime: embed|sidecar |
protean.worker.container.* |
— | image · memory · pids-limit · network · seccomp · db-host |
protean.worker.db.auto-provision |
false |
Auto-provision an isolated DB per scope (tenant); a deploy selects a scope (needs dialect + admin credentials) |
protean.worker.db.scopes / .allow-destroy |
(empty) / false |
Startup scope allowlist (empty → default) / guard for the irreversible scope destroy |
See autoconfigure/ProteanProperties.java for the full set.
There is no gradlew wrapper — use a system gradle (Java 21 toolchain).
gradle build # compile + plain jar + bootJar ('-boot')
gradle test # full test suite (maxHeap 512m — for the leak canary)
gradle :test --tests 'org.htcom.protean.XxxTest' # a single class (root project only)
gradle publishToMavenLocal # publish to ~/.m2 (POM / consumability check)Notes:
- Run bootJar and test separately. Combining them (
clean bootJar test) can trigger a collateral OOM inLeakDiagnosisTest. - Container / Testcontainers / OS-isolation / seccomp tests need Docker, and
an exploded layout from
gradle bootJarmust exist first. Without Docker those tests skip. - Artifacts: a plain jar (consumable, no classifier) +
-boot.jar(the embed-worker runnable fat jar) +-worker.jar(a flat shaded jar for the sidecar worker's process track). The sidecar worker container image ships toghcr.io/<owner>/protean-workervia./gradlew jib.
See CONTRIBUTING.md to contribute and SECURITY.md to report a vulnerability.