Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .claude/rules/00-master.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ MANDATORY: Before every session:
- Adhere to the "Model Coordination" in `01-model-coordination.md`.
- Adhere to the "GraalVM Native Image" in `10-java-standards.md`.
- Adhere to the "Hensu Java & Kotlin Standards" in `20-native-safety.md`.
- All diagrams and badges must follow the [Visual Style Guide](../../docs/visual-style-guide.md).
- Document all public APIs following the [Javadoc Guide](../../docs/javadoc-guide.md).
13 changes: 0 additions & 13 deletions .claude/rules/10-java-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,6 @@ Hensu leverages the latest JVM features to reduce boilerplate and increase type

---

## Native-Image Constraints (The "No-Go" List)

Since Hensu is optimized for **GraalVM**, you must avoid the following "dynamic" patterns:

* **No Unregistered Reflection:** Never use `Class.forName()` or `method.invoke()` unless the class is explicitly
registered in Quarkus build-time metadata.
* **No Dynamic Proxies:** Avoid libraries that generate bytecode at runtime (e.g., standard CGLIB or Hibernate
lazy-loading).
* **No Classpath Scanning:** All `AgentProvider` and `NodeExecutor` instances must be wired explicitly via
`HensuFactory.builder()`.

---

## Testing Integrity

* **Unit Tests:** Must be pure JVM and use `StubAgentProvider` to avoid API costs and network latency.
Expand Down
1 change: 0 additions & 1 deletion .cursor/rules/00-master.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ MANDATORY: Before every session:
- Adhere to the "Model Coordination" in `01-model-coordination.mdc`.
- Adhere to the "GraalVM Native Image" in `10-java-standards.mdc`.
- Adhere to the "Hensu Java & Kotlin Standards" in `20-native-safety.mdc`.
- All diagrams and badges must follow the [Visual Style Guide](../../docs/visual-style-guide.md).
- Document all public APIs following the [Javadoc Guide](../../docs/javadoc-guide.md).
13 changes: 0 additions & 13 deletions .cursor/rules/10-java-standards.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,6 @@ Hensu leverages the latest JVM features to reduce boilerplate and increase type

---

## Native-Image Constraints (The "No-Go" List)

Since Hensu is optimized for **GraalVM**, you must avoid the following "dynamic" patterns:

* **No Unregistered Reflection:** Never use `Class.forName()` or `method.invoke()` unless the class is explicitly
registered in Quarkus build-time metadata.
* **No Dynamic Proxies:** Avoid libraries that generate bytecode at runtime (e.g., standard CGLIB or Hibernate
lazy-loading).
* **No Classpath Scanning:** All `AgentProvider` and `NodeExecutor` instances must be wired explicitly via
`HensuFactory.builder()`.

---

## Testing Integrity

* **Unit Tests:** Must be pure JVM and use `StubAgentProvider` to avoid API costs and network latency.
Expand Down
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ concerns (e.g., adding a new rubric) from the core agent execution logic.
- `ExecutionLeaseManager` - Distributed lease manager; generates `server_node_id`, bumps heartbeats, atomically claims stale executions (`@ApplicationScoped`)
- `ExecutionHeartbeatJob` - Scheduled heartbeat; runs every `hensu.lease.heartbeat-interval` (default 30s) (`@ApplicationScoped`)
- `WorkflowRecoveryJob` - Scheduled sweeper; claims orphaned executions older than `hensu.lease.stale-threshold` (default 90s) and resumes them (`@ApplicationScoped`)
- `WorkflowRegistryService` - Push pipeline: wraps save in `WorkflowPushLock` and invokes `SubWorkflowGraphValidator` lazily resolving sub-workflow ids through the repository; rejects cycles and dangling refs before any row is written (`@ApplicationScoped`)
- `WorkflowPushLock` - Cluster-wide push mutex (`pg_advisory_xact_lock` with JVM `ReentrantLock` fallback) so two concurrent pushes on different nodes cannot together introduce a cycle (`@ApplicationScoped`)

**AI Provider Interface**:

Expand Down Expand Up @@ -151,7 +153,7 @@ Workflow
- `GenericNode` - Custom execution logic via registered handlers
- `ActionNode` - Execute commands mid-workflow (git, deploy, notify)
- `EndNode` - Workflow termination (SUCCESS/FAILURE/CANCELLED)
- `SubWorkflowNode` - Nested workflows
- `SubWorkflowNode` - Delegation to another workflow by id with input/output mapping; cycle + dangling-ref validation at push (`SubWorkflowGraphValidator`); depth cap `MAX_DEPTH = 16`; tenant isolation preserved via `_tenant_id` propagation

**Transitions** (`TransitionRule` interface):

Expand Down Expand Up @@ -229,6 +231,7 @@ the recovery sweeper's scope.

- `_tenant_id` — tenant identifier, read by `SubWorkflowNodeExecutor` for loading child workflows
- `_execution_id` — ensures `WorkflowExecutor` uses the same ID the service layer tracks
- `_sub_workflow_depth` — recursion depth counter enforced by `SubWorkflowNodeExecutor.MAX_DEPTH = 16`

### Patterns & Conventions

Expand Down Expand Up @@ -384,6 +387,8 @@ CRUD operations, UPSERT semantics, FK constraints, tenant isolation, and seriali
- `hensu-core/.../execution/enricher/YieldsVariableInjector.java` - Injects yield format instructions for branch prompts
- `hensu-core/.../workflow/transition/ApprovalTransition.java` - Boolean approval routing via `approved` engine variable
- `hensu-core/.../workflow/validation/WorkflowValidator.java` - Load-time validator for `writes` and prompt variable references
- `hensu-core/.../workflow/validation/SubWorkflowGraphValidator.java` - Cycle + dangling-ref detection across the sub-workflow reference graph (single DFS, `globallyVisited`); CLI overload `validate(Collection<Workflow>)` for local cycle-only checks, server overload `validate(Workflow, Function<String,Workflow>)` for push with lazy repository resolution
- `hensu-core/.../execution/executor/SubWorkflowNodeExecutor.java` - Child workflow execution with `MAX_DEPTH = 16` recursion cap and `_tenant_id` propagation

**DSL:**

Expand Down Expand Up @@ -414,8 +419,10 @@ CRUD operations, UPSERT semantics, FK constraints, tenant isolation, and seriali
- `hensu-server/src/main/resources/db/migration/V1__create_persistence_tables.sql` - Flyway schema migration
- `hensu-server/src/main/resources/db/migration/V2__add_execution_leases.sql` - Lease columns migration
- `hensu-server/.../persistence/ExecutionLeaseManager.java` - Distributed lease manager (heartbeat + atomic claim)
- `hensu-server/.../service/ExecutionHeartbeatJob.java` - Heartbeat emission (@Scheduled)
- `hensu-server/.../service/WorkflowRecoveryJob.java` - Recovery sweeper (@Scheduled)
- `hensu-server/.../persistence/WorkflowPushLock.java` - Cluster-wide push mutex (pg_advisory_xact_lock + JVM fallback)
- `hensu-server/.../workflow/ExecutionHeartbeatJob.java` - Heartbeat emission (@Scheduled)
- `hensu-server/.../workflow/WorkflowRecoveryJob.java` - Recovery sweeper (@Scheduled)
- `hensu-server/.../workflow/WorkflowRegistryService.java` - Push pipeline: `WorkflowPushLock` + `SubWorkflowGraphValidator`

**CLI:**

Expand Down
37 changes: 37 additions & 0 deletions docs/developer-guide-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This guide covers API usage, adapter development, extension points, and testing
- [Agentic Output Validation](#agentic-output-validation)
- [Creating Custom Adapters](#creating-custom-adapters)
- [Generic Nodes](#generic-nodes)
- [Sub-Workflows](#sub-workflows)
- [Action Handlers](#action-handlers)
- [Rubric Engine](#rubric-engine)
- [Score-Based Routing](#score-based-routing)
Expand Down Expand Up @@ -486,6 +487,30 @@ private void registerGenericHandlers() {
4. **Return meaningful metadata**: Include relevant info in `NodeResult` metadata map
5. **Handle errors gracefully**: Return `NodeResult.failure()` with clear error messages

## Sub-Workflows

`SubWorkflowNode` delegates execution to a nested workflow. The parent pauses on the boundary, the child runs to completion, and control returns to the parent with selected outputs mapped back into its state.

### Context propagation and depth limit

- `_tenant_id` is copied from parent into child context, preserving multi-tenant isolation across the boundary.
- Nested invocation is capped at depth 16 (`SubWorkflowNodeExecutor.MAX_DEPTH`) via `_sub_workflow_depth`. The executor throws before invoking a child beyond the cap – this is a hard guard against runaway recursion, not a tunable.

### Input and output mappings

Data crosses the boundary only through explicit mappings declared on `SubWorkflowNode`:

| Mapping | Direction | Semantics |
|-----------------|------------------------|------------------------------------------------------------------------------------------------------------------------------|
| `inputMapping` | `childKey → parentKey` | Before the child starts, the executor reads `parentKey` from parent state and writes it as `childKey` in child state. |
| `outputMapping` | `parentKey → childKey` | On successful child completion, the executor reads `childKey` from child state and writes it as `parentKey` in parent state. |

Anything not covered by these mappings stays on its side of the boundary. There is no implicit state leakage between parent and child.

### Reference graph validation

`SubWorkflowGraphValidator` rejects cycles and dangling references at graph-load time, before any node executes. See [SubWorkflowGraphValidator checks](#subworkflowgraphvalidator-checks) under State Schema.

## Action Handlers

Action handlers send data from workflow actions to external systems. Handlers can implement any integration: HTTP calls, messaging (Slack, email), event publishing (Kafka, RabbitMQ), database operations, or custom logic.
Expand Down Expand Up @@ -729,6 +754,17 @@ WorkflowValidator.validate(workflow); // throws IllegalStateException on violati

Validation is a no-op when no schema is declared. Legacy workflows always pass through unchanged.

### `SubWorkflowGraphValidator` checks

`SubWorkflowGraphValidator` runs at graph-load time over the sub-workflow reference graph. It rejects cycles and – on the server push path – unresolved references, so neither can surface mid-execution. Two overloads serve the two entry points:

| Overload | Caller | Detects | Unknown targets |
|----------------------------------|------------------|---------------------------------------|------------------------------------------------------------------------------------------------|
| `validate(Collection<Workflow>)` | CLI batch loader | Cycles only | Silently skipped – loader reports missing `--with` declarations separately with richer context |
| `validate(Workflow, Function)` | Server push path | Cycles **and** unknown referenced ids | Aggregated into a single `IllegalStateException` alongside any cycles, in a single DFS pass |

The `(Workflow, Function)` overload shadows the incoming workflow for its own id so re-push/update sees the post-push graph without an intermediate write. The resolver is queried lazily and only for ids forward-reachable from the root – bounded by a `globallyVisited` set so each id costs at most one repository lookup.

## Engine Variable Injection

Before each agent call, `AgentLifecycleRunner` runs `EngineVariablePromptEnricher` to append
Expand Down Expand Up @@ -1283,6 +1319,7 @@ Environment variables matching `*_API_KEY`, `*_KEY`, `*_SECRET`, or `*_TOKEN` pa
| `workflow/state/StateVariableDeclaration.java` | Single variable declaration record (name, type, isInput) |
| `workflow/state/VarType.java` | Variable type enum: STRING, NUMBER, BOOLEAN, LIST_STRING |
| `workflow/transition/ApprovalTransition.java` | Boolean approval routing via the `approved` engine variable |
| `workflow/validation/SubWorkflowGraphValidator.java` | Load-time cycle + dangling-reference detector for sub-workflow graphs |
| `workflow/validation/WorkflowValidator.java` | Load-time validator for `writes` and prompt `{variable}` references |
| `rubric/RubricEngine.java` | Quality evaluation engine |
| `rubric/model/Rubric.java` | Rubric definition model |
Expand Down
78 changes: 71 additions & 7 deletions docs/developer-guide-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This guide covers the architecture, patterns, and best practices for developing
- [Package Structure](#package-structure)
- [Multi-Tenancy](#multi-tenancy)
- [REST API Development](#rest-api-development)
- [Sub-Workflow Validation on Push](#sub-workflow-validation-on-push)
- [SSE Streaming](#sse-streaming)
- [MCP Integration](#mcp-integration)
- [Dynamic Tool Discovery](#dynamic-tool-discovery)
Expand Down Expand Up @@ -296,9 +297,16 @@ io.hensu.server/
│ └── ConstraintViolationExceptionMapper # Global 400 error mapper
├── config/ # CDI configuration
│ ├── HensuEnvironmentProducer # HensuFactory → HensuEnvironment
│ ├── ServerBootstrap # Startup registrations
│ └── ServerConfiguration # CDI delegation + server beans
│ ├── HensuEnvironmentProducer # HensuFactory → HensuEnvironment
│ ├── NativeImageConfig # @RegisterForReflection — Hensu domain model
│ ├── LangChain4jNativeConfig # @RegisterForReflection — JDK HTTP transport
│ ├── LangChain4jAnthropicNativeConfig # @RegisterForReflection — Anthropic DTOs
│ ├── LangChain4jGeminiNativeConfig # @RegisterForReflection — Gemini DTOs
│ ├── ServerBootstrap # Startup registrations
│ └── ServerConfiguration # CDI delegation + server beans
├── dev/ # Dev-only handlers (excluded from prod image)
│ └── SleepHandler # Simulates long-running node for crash-recovery tests
├── execution/ # Server-side execution listeners
│ ├── LoggingExecutionListener # Logs plan/step lifecycle events
Expand All @@ -308,21 +316,37 @@ io.hensu.server/
│ ├── JsonRpc # JSON-RPC 2.0 message helper
│ ├── McpSessionManager # SSE session management
│ ├── McpConnection # Connection interface
│ ├── McpConnectionFactory # Factory for per-tenant connections
│ ├── McpConnectionPool # Connection pooling
│ ├── McpException # Checked MCP protocol errors
│ ├── McpSidecar # ActionHandler for MCP tools
│ └── SseMcpConnection # SSE-based connection impl
│ ├── McpToolDiscovery # Runtime tool schema discovery + cache
│ ├── SseMcpConnection # SSE-based connection impl
│ └── TenantToolRegistry # Merges base + tenant MCP tools (MCP precedence)
├── security/ # JWT + tenant resolution + error mapping
│ ├── GlobalExceptionMapper # Global @Provider — normalizes errors to JSON
│ └── RequestTenantResolver # Extracts tenant_id claim from JWT
├── persistence/ # PostgreSQL persistence (plain JDBC)
│ ├── JdbcWorkflowRepository # Workflow definitions (JSONB)
│ ├── JdbcWorkflowStateRepository # Execution state snapshots (JSONB + lease columns)
│ ├── ExecutionLeaseManager # Distributed lease management (@ApplicationScoped)
│ ├── WorkflowPushLock # Cluster-wide push mutex (pg_advisory_xact_lock + JVM fallback)
│ ├── JdbcSupport # JDBC helper (queryList, update)
│ └── PersistenceException # Unchecked wrapper for SQLException
├── service/ # Business logic layer
│ ├── WorkflowService # Workflow operations
├── workflow/ # Business logic layer
│ ├── WorkflowService # Facade over registry + execution + query services
│ ├── WorkflowRegistryService # Push pipeline: WorkflowPushLock + SubWorkflowGraphValidator
│ ├── WorkflowExecutionService # Start/resume orchestration
│ ├── ExecutionQueryService # Read-side: status, plan, output, paused list
│ ├── ExecutionStateService # Snapshot load/save with split-brain guard
│ ├── ExecutionHeartbeatJob # Periodic heartbeat emission (@Scheduled)
│ └── WorkflowRecoveryJob # Orphaned execution sweeper (@Scheduled)
│ ├── WorkflowRecoveryJob # Orphaned execution sweeper (@Scheduled)
│ ├── ExecutionStartResult / ExecutionOutput / ExecutionSummary / PlanInfo / ResumeDecision # DTOs
│ ├── ExecutionStatus # Enum: COMPLETED / PAUSED / RUNNING
│ └── {Execution,Workflow}{NotFound,Execution}Exception # Domain exceptions
├── streaming/ # Execution event streaming
│ ├── ExecutionEvent # Event DTOs (sealed interface)
Expand Down Expand Up @@ -547,6 +571,46 @@ public Response push(@ValidWorkflow Workflow workflow) {
}
```

#### Sub-Workflow Validation on Push

`ValidWorkflowValidator` checks the incoming workflow in isolation. Cross-workflow
consistency — cycles and dangling sub-workflow references — is a second pass handled by
`WorkflowRegistryService` before the row is written:

```java
// WorkflowRegistryService.push()
pushLock.runExclusive(tenantId, workflowId, () -> {
SubWorkflowGraphValidator.validate(
workflow,
id -> id.equals(workflow.getId())
? Optional.of(workflow) // incoming overrides repo
: repository.findById(tenantId, id)); // lazy resolution
repository.save(tenantId, workflow);
});
```

Key properties:

- **Cluster-wide mutex** — `WorkflowPushLock` wraps the save in `pg_advisory_xact_lock`
(JVM `ReentrantLock` fallback when `quarkus.datasource.active=false`). Without this,
two concurrent pushes on different nodes could each observe a clean graph and together
introduce a cycle.
- **Post-push view** — the resolver short-circuits to the incoming workflow for its own
id, so the DFS sees the graph *as it will exist* after the push — no intermediate write
is needed, and a push that would fix an existing cycle validates correctly.
- **Single DFS** — `SubWorkflowGraphValidator.validate(Workflow, Function)` uses one
`globallyVisited` set for both cycle detection and dangling-ref detection.
- **Tenant-scoped resolution** — `repository.findById(tenantId, id)` never looks up
workflows from another tenant, so a sub-workflow target that exists under a different
tenant is rejected as dangling.
- **Runtime bounds** — even if validation passes, recursion is capped at
`SubWorkflowNodeExecutor.MAX_DEPTH = 16` (tracked via `_sub_workflow_depth`) and
`_tenant_id` is propagated into the child context, so a malicious deep chain cannot
exhaust the stack and a child cannot escape its tenant boundary.

See the [hensu-core Developer Guide — `SubWorkflowGraphValidator` checks](developer-guide-core.md#subworkflowgraphvalidator-checks)
for the core-side algorithm and the CLI-only `validate(Collection<Workflow>)` overload.

#### Log Sanitizer (Defense-in-Depth)

`LogSanitizer.sanitize()` strips CR/LF characters from user-derived values before they reach log
Expand Down
Loading
Loading