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
65 changes: 65 additions & 0 deletions jamjet-agent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>dev.jamjet</groupId>
<artifactId>jamjet-runtime-java-parent</artifactId>
<version>0.3.1</version>
</parent>

<artifactId>jamjet-agent</artifactId>
<packaging>jar</packaging>

<name>JamJet Agent (Java)</name>
<description>
Idiomatic Java agent authoring on the governed durable JamJet engine: a thin
Agent builder that compiles to the shared agent-loop WorkflowIr plus a durable
Java tool-worker. Plain Java 21 (virtual threads), framework-free. This module
owns the bare-route engine HTTP client (workflows, executions, work-items).
</description>

<dependencies>
<!-- Typed IR + the shared snake_case JamjetJson mapper (Rust-wire-compatible). -->
<dependency>
<groupId>dev.jamjet</groupId>
<artifactId>jamjet-runtime-core</artifactId>
<version>${project.version}</version>
</dependency>

<!-- JSON: same Jackson the parent project already pins. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Comment on lines +33 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Jackson version declarations and imports:"
rg -n --glob 'pom.xml' 'jackson-databind|jackson-bom|jackson.version'

echo
echo "POM context around Jackson-related entries:"
fd -a '^pom\.xml$' . -x sh -c '
  if rg -q "jackson-databind|jackson-bom|jackson.version" "$1"; then
    echo "=== $1 ==="
    nl -ba "$1" | sed -n "/jackson/,+20p"
  fi
' sh {}

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Top-level files:"
git ls-files | sed -n '1,200p'

echo
echo "POM files:"
git ls-files '*pom.xml'

echo
echo "Jackson-related matches across tracked POMs:"
rg -n --glob '**/pom.xml' 'jackson-databind|jackson-bom|jackson.version|jackson-core|jackson-annotations' .

echo
echo "jamjet-agent/pom.xml context:"
nl -ba jamjet-agent/pom.xml | sed -n '1,120p'

echo
echo "Parent POM candidates:"
for f in $(git ls-files '*pom.xml'); do
  if grep -q '<packaging>pom</packaging>' "$f" 2>/dev/null; then
    echo "=== $f ==="
    nl -ba "$f" | sed -n '1,220p'
  fi
done

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 16692


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Root pom.xml Jackson section:"
python3 - <<'PY'
from pathlib import Path
p = Path("pom.xml")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 55 <= i <= 85:
        print(f"{i}: {line}")
PY

echo
echo "jamjet-agent/pom.xml around the dependency:"
python3 - <<'PY'
from pathlib import Path
p = Path("jamjet-agent/pom.xml")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 25 <= i <= 45:
        print(f"{i}: {line}")
PY

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 2447


🌐 Web query:

Jackson databind 2.17.2 advisories CVE high severity 2025

💡 Result:

As of June 28, 2026, Jackson Databind 2.17.2 is affected by several high-severity security vulnerabilities discovered in 2025 and 2026. Because 2.17.2 is no longer the latest maintained version, it remains vulnerable to these issues, and users are strongly advised to upgrade to a patched version (such as 2.18.8 or higher) [1][2][3][4]. Key vulnerabilities affecting version 2.17.2 include: 1. PolymorphicTypeValidator (PTV) Bypass (CVE-2026-54512): This high-severity vulnerability allows an attacker to bypass the PTV allow-list by using generic type parameters (e.g., ArrayList<com.evil.Gadget>), potentially leading to arbitrary class instantiation and remote code execution [1][2][5]. It affects all versions from 2.10.0 up to 2.18.7 [1][5]. 2. Array Subtype Allowlist Bypass (CVE-2026-54513): This issue exists in the BasicPolymorphicTypeValidator's allowIfSubTypeIsArray method, which fails to validate the component type of an array, allowing an attacker to instantiate non-allowlisted types [3]. It affects versions 2.10.0 through 2.18.7 [3]. 3. SSRF via InetSocketAddress (CVE-2026-54514): Deserializing InetSocketAddress objects triggers eager DNS resolution, which can be exploited for Server-Side Request Forgery (SSRF) or internal-resolver probing. This affects versions 2.0.0 through 2.18.7 [4]. Note on CVE-2025-52999: While widely reported as a high-severity denial-of-service vulnerability affecting jackson-core, it primarily impacts versions prior to 2.15.0 [6][7]. Jackson Databind 2.17.2 uses a more recent jackson-core and is not directly vulnerable to this specific stack-overflow issue [6]. To remediate these risks, you should update your project dependencies to use Jackson Databind 2.18.8 or a newer release [1][2][3][4].

Citations:


Bump the parent Jackson baseline. jamjet-agent/pom.xml inherits jackson-databind from the root BOM, and pom.xml:61-79 still pins jackson.version to 2.17.2. Upgrade that shared version to a patched release (for example 2.18.8+) so this module and the rest of the build stop pulling the vulnerable databind line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jamjet-agent/pom.xml` around lines 33 - 36, The shared Jackson baseline is
still pinned to an older vulnerable databind release, so update the root
`jackson.version` property in the BOM that `jamjet-agent` inherits from to a
patched version such as `2.18.8` or newer. Make the change in the shared Maven
property, not in the `jackson-databind` dependency declaration itself, so all
modules using the BOM pick up the safer version consistently.

Source: Linters/SAST tools


<!-- HTTP: java.net.http (built-in JDK 11+). No extra dep. -->

<!-- Test scope -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Hermetic engine stub for the bare-route round-trip test. -->
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>3.9.2</version>
<scope>test</scope>
</dependency>
<!-- SLF4J binding so WireMock/Jetty logging has a provider in tests. -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
329 changes: 329 additions & 0 deletions jamjet-agent/src/main/java/dev/jamjet/agent/Agent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
package dev.jamjet.agent;

import dev.jamjet.agent.client.JamjetEngineClient;
import dev.jamjet.agent.tools.ToolRegistry;
import dev.jamjet.runtime.core.ir.PolicySetIr;
import dev.jamjet.runtime.core.ir.WorkflowIr;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* Idiomatic Java authoring for a governed, durable JamJet agent — the Java analog
* of the Python {@code jamjet.Agent}. A {@code model} + {@code @Tool} methods +
* {@code instructions} + governance knobs, compiled by {@link #compileToIr()} to
* the <em>same</em> agent-loop {@link WorkflowIr} the Python ADK emits, so a Java
* agent and a Python agent run on the identical Rust engine (the Java tool nodes
* being {@code java_fn} where Python's are {@code python_fn}).
*
* <p>Construct via the {@link #builder(String)} fluent builder:
* <pre>{@code
* Agent agent = Agent.builder("research_agent")
* .model("anthropic/claude-sonnet-4-6")
* .instructions("You are a helpful research assistant.")
* .tools(new WebSearchTools(), new MathTools())
* .policy(new PolicySetIr(List.of("delete_db"), List.of(), List.of()))
* .approvalRequired(List.of("delete_*"))
* .budget(new Budget(100_000, 2.5))
* .build();
*
* WorkflowIr ir = agent.compileToIr(); // the durable agent-loop IR
* }</pre>
*
* <p>The model side is free: a Java agent emits only {@code Model} nodes; the Rust
* engine routes model calls through the governed Python model-seam sidecar. No
* Java model code is needed.
*/
public final class Agent {

/** Default static-unroll bound for the agent loop (mirrors the Python default). */
public static final int DEFAULT_MAX_TURNS = 8;

private final String name;
private final String model;
private final String instructions;
private final String strategy;
private final ToolRegistry registry;
private final PolicySetIr policy;
private final boolean approvalAll;
private final List<String> approvalGlobs;
private final Budget budget;
private final boolean pii;
private final int timeoutSeconds;

private Agent(Builder b) {
this.name = b.name;
this.model = b.model;
this.instructions = b.instructions;
this.strategy = b.strategy;
this.registry = b.registry;
this.policy = b.policy;
this.approvalAll = b.approvalAll;
this.approvalGlobs = List.copyOf(b.approvalGlobs);
this.budget = b.budget;
this.pii = b.pii;
this.timeoutSeconds = b.timeoutSeconds;
}

/** Start building an agent with the given logical name (used as the workflow id). */
public static Builder builder(String name) {
return new Builder(name);
}

/**
* Compile this agent to the durable agent-loop {@link WorkflowIr} with the
* default unroll bound ({@link #DEFAULT_MAX_TURNS}).
*/
public WorkflowIr compileToIr() {
return compileToIr(DEFAULT_MAX_TURNS);
}

/**
* Compile this agent to the durable agent-loop {@link WorkflowIr}, statically
* unrolling up to {@code maxTurns} {@code model -> tools} turns plus a final
* tool-less answer turn. The start node is always the first Model node.
*/
public WorkflowIr compileToIr(int maxTurns) {
return AgentIrCompiler.compile(this, maxTurns);
}

// -- durable run (B-4) ------------------------------------------------------

/**
* Run this agent durably on the JamJet engine with the default {@link RunOptions}
* (local runtime, {@link #DEFAULT_MAX_TURNS} turns), returning the final assistant
* text + tool-call trace as an {@link AgentResult}. The Java mirror of the Python
* {@code Agent.run_durable}.
*
* <p>This compiles the agent to the agent-loop {@link WorkflowIr}, registers it
* ({@code POST /workflows}), starts an execution seeded with the system+user
* {@code messages} ({@code POST /executions}), polls {@code GET /executions/{id}}
* to a terminal state, and extracts the answer from the terminal
* {@code current_state.last_model_output} (falling back to the last assistant
* message). Every model call and tool dispatch runs through the durable engine, so
* the run is event-sourced, replayable, idempotent, and governed: budget and policy
* (the model allowlist plus approval gates) are enforced fail-closed by the engine,
* and PII redaction is applied at the model-seam sidecar (the {@code data_policy} IR
* signals it; redaction is the sidecar's job, not an IR-level guarantee).
*
* <h2>Required running services (mirrors the Python {@code run_durable})</h2>
* A durable run is NOT self-contained — three services must be running:
* <ol>
* <li><b>the JamJet engine</b> at {@link RunOptions#runtimeUrl()} (the
* {@code jamjet-server} that owns the {@code java_tool} queue);</li>
* <li><b>the model sidecar</b> ({@code JAMJET_MODEL_SEAM_URL}) — the engine routes
* every governed model call through it (no Java model code);</li>
* <li><b>a {@link dev.jamjet.agent.worker.JavaToolWorker}</b> draining the
* {@code java_tool} queue with THIS agent's tool registry, so the
* {@code @Tool} methods execute durably exactly-once. Run it in a separate
* thread/process; {@code runDurable} does not start one.</li>
* </ol>
*
* @throws AgentRunException if the run reaches a non-{@code completed} terminal
* state ({@code failed} / {@code cancelled} /
* {@code limit_exceeded})
* @throws AgentRunTimeoutException if no terminal state is reached before the deadline
*/
public AgentResult runDurable(String prompt) {
return runDurable(prompt, RunOptions.defaults());
}

/**
* Run this agent durably with the given {@link RunOptions}, building (and closing) a
* {@link JamjetEngineClient} for {@link RunOptions#runtimeUrl()}. See
* {@link #runDurable(String)} for the running-services contract.
*/
public AgentResult runDurable(String prompt, RunOptions options) {
try (JamjetEngineClient client =
new JamjetEngineClient(options.runtimeUrl(), options.bearerToken(), options.tenantId())) {
return runDurable(prompt, client, options);
}
}

/**
* Run this agent durably over a caller-provided {@link JamjetEngineClient}. The
* caller owns the client's lifecycle (this overload does NOT close it), so a run
* and a {@link dev.jamjet.agent.worker.JavaToolWorker} can share one client against
* the same engine. See {@link #runDurable(String)} for the running-services contract.
*/
public AgentResult runDurable(String prompt, JamjetEngineClient client, RunOptions options) {
return DurableRunner.run(this, prompt, client, options);
}

// -- accessors (read by AgentIrCompiler) ------------------------------------

public String name() {
return name;
}

public String model() {
return model;
}

public String instructions() {
return instructions;
}

public String strategy() {
return strategy;
}

public ToolRegistry registry() {
return registry;
}

/** The inline policy, or {@code null} when none is set. */
public PolicySetIr policy() {
return policy;
}

/** {@code true} when every tool call requires human approval ({@code approvalRequired(true)}). */
public boolean approvalAll() {
return approvalAll;
}

/** The per-tool approval globs (possibly empty). */
public List<String> approvalGlobs() {
return approvalGlobs;
}

/** The per-run budget, or {@code null} when uncapped. */
public Budget budget() {
return budget;
}

/** Whether PII governance is on (emits the default {@code data_policy} IR). */
public boolean pii() {
return pii;
}

/** The workflow timeout in seconds (compiled into {@code timeouts.workflow_timeout}). */
public int timeoutSeconds() {
return timeoutSeconds;
}

@Override
public String toString() {
return "Agent(name=" + name + ", model=" + model
+ ", tools=" + registry.tools().stream().map(t -> t.name()).toList()
+ ", strategy=" + strategy + ")";
}

/** Fluent builder for {@link Agent}. */
public static final class Builder {
private final String name;
private String model;
private String instructions = "";
private String strategy = "react";
private ToolRegistry registry = new ToolRegistry();
private PolicySetIr policy;
private boolean approvalAll;
private List<String> approvalGlobs = List.of();
private Budget budget;
private boolean pii = true;
private int timeoutSeconds = 300;

private Builder(String name) {
this.name = Objects.requireNonNull(name, "agent name must not be null");
}

/** The model reference, e.g. {@code "anthropic/claude-sonnet-4-6"}. */
public Builder model(String model) {
this.model = model;
return this;
}

/** The system instructions for the agent. */
public Builder instructions(String instructions) {
this.instructions = instructions == null ? "" : instructions;
return this;
}

/**
* The reasoning strategy. v1 always compiles the durable IR as the
* react-style {@code model -> tools -> model} loop regardless of this
* value (parity with the Python durable path); the field is carried for
* forward compatibility.
*/
public Builder strategy(String strategy) {
this.strategy = strategy == null ? "react" : strategy;
return this;
}

/** The tool-holder instances whose {@code @Tool} methods the agent may call. */
public Builder tools(Object... holders) {
this.registry = ToolRegistry.of(holders);
return this;
}

/** The tool-holder instances whose {@code @Tool} methods the agent may call. */
public Builder tools(List<?> holders) {
this.registry = ToolRegistry.of(holders);
return this;
}

/** Use a pre-built tool registry. */
public Builder registry(ToolRegistry registry) {
this.registry = registry == null ? new ToolRegistry() : registry;
return this;
}

/** An inline policy ({@code blocked_tools} / {@code require_approval_for} / {@code model_allowlist}). */
public Builder policy(PolicySetIr policy) {
this.policy = policy;
return this;
}

/**
* Require human approval for tools. {@code true} requires approval for
* every tool call ({@code require_approval_for = ["*"]}); {@code false}
* clears the gate.
*/
public Builder approvalRequired(boolean all) {
this.approvalAll = all;
this.approvalGlobs = List.of();
return this;
}

/**
* Require human approval for the given tool-name globs (e.g.
* {@code ["delete_*", "send_*"]}). Unioned into {@code require_approval_for}.
*/
public Builder approvalRequired(List<String> globs) {
this.approvalAll = false;
this.approvalGlobs = globs == null ? List.of() : new ArrayList<>(globs);
return this;
}

/** The per-run budget (token and/or cost cap). */
public Builder budget(Budget budget) {
this.budget = budget;
return this;
}

/** Toggle PII governance (on by default). */
public Builder pii(boolean pii) {
this.pii = pii;
return this;
}

/** The workflow timeout in seconds (default 300; must be positive). */
public Builder timeoutSeconds(int timeoutSeconds) {
if (timeoutSeconds <= 0) {
throw new IllegalArgumentException("timeoutSeconds must be positive (got " + timeoutSeconds + ")");
}
this.timeoutSeconds = timeoutSeconds;
return this;
}

/** Build the immutable {@link Agent}. */
public Agent build() {
Objects.requireNonNull(model, "agent model must be set");
if (model.isBlank()) {
throw new IllegalArgumentException("agent model must not be blank");
}
return new Agent(this);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
Loading
Loading