Skip to content

Java SDK ​

The APS Java implementation is a multi-module Maven project. All modules are pre-release and track the specification draft.

Repository: agentpolicyspecification/aps-java

Modules ​

ModuleDescription
coreEngine, policy interfaces, types, errors
dslJSON-based rule policies
regoRego policies via OPA WASM (in-process, Chicory)
opaRego policies via OPA REST API
httpRemote policy evaluation over HTTP
otelOpenTelemetry audit handler
langfuseLangfuse audit handler

core ​

The foundation module. Provides the ApsEngine, policy interfaces, context and decision types, and error classes.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>core</artifactId>
  <version>0.1.0</version>
</dependency>

ApsEngine ​

ApsEngine evaluates a PolicySet at each interception point. Construct it once and reuse it across requests.

java
import io.github.agentpolicyspecification.core.ApsEngine;
import io.github.agentpolicyspecification.core.PolicySet;

ApsEngine engine = ApsEngine.builder()
    .policySet(PolicySet.builder()
        .input(List.of(/* InputPolicy... */))
        .toolCall(List.of(/* ToolCallPolicy... */))
        .output(List.of(/* OutputPolicy... */))
        .onError(OnErrorBehavior.DENY)
        .build())
    .onAudit(record -> System.out.println("[audit] " + record))
    .build();

Call the engine at each interception point:

java
engine.evaluateInput(inputContext);      // before sending to the LLM
engine.evaluateToolCall(toolCallCtx);    // before executing a tool
engine.evaluateOutput(outputContext);    // after the LLM responds

Denied decisions throw PolicyDenialError. Evaluation errors throw PolicyEvaluationError.

Policy interfaces ​

Implement one of three interfaces depending on the interception point you need:

java
import io.github.agentpolicyspecification.core.policy.InputPolicy;
import io.github.agentpolicyspecification.core.context.InputContext;
import io.github.agentpolicyspecification.core.decision.PolicyDecision;
import io.github.agentpolicyspecification.core.decision.DenyDecision;
import io.github.agentpolicyspecification.core.decision.AllowDecision;

public class NoSsnPolicy implements InputPolicy {

    @Override
    public String id() { return "no-ssn"; }

    @Override
    public PolicyDecision evaluate(InputContext ctx) {
        boolean found = ctx.messages().stream()
            .anyMatch(m -> m.content().matches(".*\\b\\d{3}-\\d{2}-\\d{4}\\b.*"));
        return found
            ? DenyDecision.deny("Message contains a potential SSN.")
            : AllowDecision.allow();
    }
}

PolicyDecision ​

Every evaluate() call returns a PolicyDecision:

DecisionMeaning
AllowDecisionRequest proceeds
DenyDecisionRequest is blocked; PolicyDenialError is thrown
AuditDecisionRequest proceeds, audit record is emitted
RedactDecisionRequest proceeds; adapter should redact sensitive fields
TransformDecisionRequest proceeds with transformed values

Error handling ​

java
import io.github.agentpolicyspecification.core.error.PolicyDenialError;
import io.github.agentpolicyspecification.core.error.PolicyEvaluationError;

try {
    engine.evaluateInput(ctx);
} catch (PolicyDenialError e) {
    System.out.println("Blocked by policy: " + e.policyId() + " — " + e.getMessage());
}

Audit handler ​

Pass onAudit to receive a structured record for every policy decision:

java
ApsEngine engine = ApsEngine.builder()
    .policySet(...)
    .onAudit(record -> {
        // record.policyId(), record.decision(), record.interceptionPoint(),
        // record.reason(), record.context(), record.timestamp()
        logger.info("aps.audit {}", record);
    })
    .build();

dsl ​

Write policies as plain JSON files — no Java compilation needed.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>dsl</artifactId>
  <version>0.1.0</version>
</dependency>

Rule file ​

Create a JSON file in your policy directory:

json
{
  "condition": {
    "field": "messages.0.content",
    "contains": ["password", "ssn", "social security"]
  },
  "action": "deny",
  "reason": "Message contains potentially sensitive information."
}

Usage ​

java
import io.github.agentpolicyspecification.dsl.DslInputPolicy;
import io.github.agentpolicyspecification.dsl.DslOutputPolicy;

ApsEngine engine = ApsEngine.builder()
    .policySet(PolicySet.builder()
        .input(List.of(new DslInputPolicy("no-pii", "./policies")))
        .output(List.of(new DslOutputPolicy("no-secrets", "./policies")))
        .build())
    .build();

The file name (without .json) is the policy id.

Condition types ​

ConditionExample
contains{ "field": "messages.0.content", "contains": ["secret"] }
equals{ "field": "tool_name", "equals": "delete_file" }
not_in{ "field": "tool_name", "not_in": ["read_file", "list_dir"] }
greater_than{ "field": "messages.length", "greater_than": 10 }
always{ "always": true }

rego ​

Evaluate Rego policies compiled to WebAssembly, in-process via Chicory. No external server required.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>rego</artifactId>
  <version>0.1.0</version>
</dependency>

Build the bundle ​

bash
opa build --target=wasm --entrypoint=aps/input/decision \
  ./policies/no-ssn.rego --output ./policies/no-ssn.tar.gz

Usage ​

java
import io.github.agentpolicyspecification.rego.RegoInputPolicy;

ApsEngine engine = ApsEngine.builder()
    .policySet(PolicySet.builder()
        .input(List.of(new RegoInputPolicy("no-ssn", "./policies/no-ssn.tar.gz")))
        .build())
    .build();

opa ​

Evaluate policies via a running OPA server. Suitable for centralised policy management.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>opa</artifactId>
  <version>0.1.0</version>
</dependency>

Start OPA and load a policy ​

bash
opa run --server --addr=:8181
curl -X PUT http://localhost:8181/v1/policies/no-ssn \
  --data-binary @./policies/no-ssn.rego

Usage ​

java
import io.github.agentpolicyspecification.opa.OpaInputPolicy;
import io.github.agentpolicyspecification.opa.OpaClientOptions;

ApsEngine engine = ApsEngine.builder()
    .policySet(PolicySet.builder()
        .input(List.of(new OpaInputPolicy(
            "no-ssn",
            new OpaClientOptions("http://localhost:8181"),
            "aps/input")))
        .build())
    .build();

http ​

Delegate policy evaluation to a remote HTTP endpoint.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>http</artifactId>
  <version>0.1.0</version>
</dependency>

Protocol ​

The module POSTs to {baseUrl}/aps/evaluate:

json
{
  "policy_id": "no-ssn",
  "interception_point": "input",
  "context": { ... }
}

The server must respond with a PolicyDecision object.

Usage ​

java
import io.github.agentpolicyspecification.http.HttpInputPolicy;
import io.github.agentpolicyspecification.http.HttpClientOptions;

ApsEngine engine = ApsEngine.builder()
    .policySet(PolicySet.builder()
        .input(List.of(new HttpInputPolicy(
            "no-ssn",
            new HttpClientOptions("https://policies.example.com"))))
        .build())
    .build();

otel ​

Emit APS audit records as OpenTelemetry spans.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>otel</artifactId>
  <version>0.1.0</version>
</dependency>

Usage ​

java
import io.github.agentpolicyspecification.otel.OtelAuditHandlers;
import io.opentelemetry.api.GlobalOpenTelemetry;

ApsEngine engine = ApsEngine.builder()
    .policySet(...)
    .onAudit(OtelAuditHandlers.create())
    .build();

Pass a custom Tracer to use your own provider:

java
.onAudit(OtelAuditHandlers.create(myTracer))

Each audit record becomes a span named aps.policy.evaluate with attributes:

AttributeValue
aps.policy_idThe policy id
aps.decisionallow, deny, audit, …
aps.interception_pointinput, tool_call, output
aps.reasonDenial reason (if present)

langfuse ​

Send APS audit records to Langfuse as traces via OTLP/HTTP.

Installation ​

xml
<dependency>
  <groupId>io.github.agentpolicyspecification</groupId>
  <artifactId>langfuse</artifactId>
  <version>0.1.0</version>
</dependency>

Usage ​

java
import io.github.agentpolicyspecification.langfuse.LangfuseAuditHandlers;
import io.github.agentpolicyspecification.langfuse.LangfuseAuditHandlerOptions;

LangfuseAuditHandler auditHandler = LangfuseAuditHandlers.create(
    LangfuseAuditHandlerOptions.builder()
        .publicKey(System.getenv("LANGFUSE_PUBLIC_KEY"))
        .secretKey(System.getenv("LANGFUSE_SECRET_KEY"))
        .build()
);

ApsEngine engine = ApsEngine.builder()
    .policySet(...)
    .onAudit(auditHandler)
    .build();

// Flush before shutdown to ensure all spans are exported
auditHandler.flush();

Session and agent metadata from the context is forwarded as session.id and user.id span attributes, enabling trace grouping in the Langfuse UI.

Credentials can also be provided via environment variables LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_BASE_URL (default: https://cloud.langfuse.com):

java
LangfuseAuditHandler auditHandler = LangfuseAuditHandlers.create();

Released under the Apache 2.0 License.