Skip to content
Open
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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# AGENTS.md

- Prefer minimal Java: records, switch expressions, `var`, fail-fast guards, and small methods.
- Keep nesting shallow. Extract private methods instead of stacking conditionals.
- Use built-in JDK features before adding framework code.
- Keep plugins and tools read-only by default unless mutation is explicitly required.
- Favor the smallest change that preserves tests and clarity.
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build & Test Commands

```bash
./gradlew build # Build + run tests
./gradlew test # Run all tests (JUnit 5 + Mockito)
./gradlew jar # Build fat JAR at build/libs/openclaw-java.jar
./gradlew test --tests "ai.openclaw.tool.CodeExecutionToolTest" # Run single test class
./gradlew test --tests "ai.openclaw.tool.CodeExecutionToolTest.testBlocksDangerousCommands" # Single test method
```

Run the gateway: `java -jar build/libs/openclaw-java.jar gateway`

## Architecture

This is an MVP Java port of [OpenClaw](https://github.com/openclaw/openclaw), a personal AI assistant. Java 21+ required (virtual threads).

### Core Flow

`Main` → picocli CLI (`OpenClawCli`) → `GatewayCommand` starts all components:
1. **ConfigLoader** reads `~/.openclaw-java/config.json` (or env vars `ANTHROPIC_API_KEY`, `GATEWAY_PORT`, `GATEWAY_AUTH_TOKEN`)
2. **GatewayServer** — WebSocket server (Java-WebSocket lib) with JSON-RPC protocol and token-based auth
3. **RpcRouter** dispatches methods (`gateway.health`, `agent.send`) to handlers
4. **AgentExecutor** — agentic loop: sends messages to LLM, handles tool_use responses in a loop (max 10 iterations), persists all messages to session
5. **ConsoleChannel** — stdin/stdout interactive channel for local use

### Key Interfaces

- **`LlmProvider`** (`agent/`) — LLM backend interface with `complete()` and `completeWithTools()`. `AnthropicProvider` implements it using raw OkHttp calls to the Anthropic Messages API.
- **`Tool`** (`tool/`) — Agent tool interface: `name()`, `description()`, `inputSchema()` (JSON Schema), `execute(JsonNode)` → `ToolResult`. Implementations: `CodeExecutionTool`, `FileReadTool`, `FileWriteTool`, `WebSearchTool`.
- **`Channel`** (`channel/`) — Messaging channel interface. Only `ConsoleChannel` is implemented.

### Session/Message Model

`SessionStore` holds in-memory sessions with JSONL file persistence. `Message` supports multiple roles: `user`, `assistant`, `system`, `assistant_tool_use` (assistant messages with tool_use content blocks), and `tool_result` (tool responses with `toolUseId`). The `AnthropicProvider` merges consecutive `tool_result` messages into a single `user` message per the Anthropic API contract.

### Config

`OpenClawConfig` is a Jackson-deserialized POJO with nested `GatewayConfig` (port, authToken) and `AgentConfig` (provider, apiKey, model, systemPrompt). All config classes use `@JsonIgnoreProperties(ignoreUnknown = true)`.

### Dependencies

Jackson (JSON), picocli (CLI), Java-WebSocket (gateway), OkHttp (HTTP client for Anthropic API), Logback/SLF4J (logging). Shared `ObjectMapper` via `Json.mapper()`.

## Conventions

- Package root: `ai.openclaw`
- All JSON serialization goes through `ai.openclaw.config.Json.mapper()` singleton
- Tools define their own JSON Schema via `inputSchema()` method
- `CodeExecutionTool` has safety patterns: blocked patterns (dangerous commands) and warned patterns (risky commands)
- Gateway uses constant-time token comparison for auth
18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
FROM gradle:8-jdk21 AS build
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY gradlew .
COPY gradle ./gradle
# Download dependencies first (cached layer)
RUN gradle dependencies --no-daemon || true
COPY build.gradle.kts settings.gradle.kts gradle.properties ./
RUN ./gradlew dependencies --no-daemon || true
COPY src ./src
RUN gradle jar --no-daemon
RUN ./gradlew jar --no-daemon

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# Install bash for CodeExecutionTool
RUN apk add --no-cache bash curl
RUN apk add --no-cache bash curl github-cli su-exec

# Create a non-root user with a dedicated workspace for tool execution
RUN addgroup -S openclaw && adduser -S openclaw -G openclaw \
&& mkdir -p /home/openclaw/workspace \
&& chown -R openclaw:openclaw /home/openclaw

COPY --from=build /app/build/libs/*.jar /app/openclaw.jar
COPY entrypoint.sh /app/entrypoint.sh

# Default gateway port
ENV GATEWAY_PORT=18789
EXPOSE ${GATEWAY_PORT}

# Run as non-root user
USER openclaw
# Run as non-root user (entrypoint needs root briefly to import certs, then drops)
ENV HOME=/home/openclaw

ENTRYPOINT ["java", "-jar", "/app/openclaw.jar"]
ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
CMD ["gateway"]
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ This project implements a minimal viable version of OpenClaw in Java, focusing o
```json
{
"gateway": { "port": 18789, "authToken": "test-token" },
"agent": { "provider": "anthropic", "apiKey": "sk-ant-...", "model": "claude-sonnet-4-20250514" }
"agent": { "provider": "anthropic", "apiKey": "sk-ant-...", "model": "claude-sonnet-4-20250514" },
"plugins": [
{
"type": "qmd",
"config": {
"command": "qmd",
"workingDirectory": "/Users/you/github",
"timeoutSeconds": 20
}
}
]
}
```

Expand All @@ -47,6 +57,28 @@ This project implements a minimal viable version of OpenClaw in Java, focusing o
- `src/main/java/ai/openclaw/config` - Configuration loader
- `src/main/java/ai/openclaw/session` - Session storage

## Plugins

Plugins are typed integrations that register one or more tools from config.

- `qmd` adds a read-only `qmd_memory` tool backed by the `qmd` CLI.
- Configure plugins under top-level `plugins`.
- Drop external plugin jars into `~/.openclaw-java/plugins`; discovery uses Java `ServiceLoader`.
- If a plugin is explicitly configured and its dependency is missing, gateway startup fails fast with a clear error.

The `qmd_memory` tool supports these operations:
- `query`
- `search`
- `vsearch`
- `get`
- `multi_get`
- `ls`
- `status`
- `context_list`
- `context_check`

For simple command wrappers that do not need a dedicated plugin class, keep using `agent.customTools`.

## Running with Docker

You can run the application in a Docker container for an isolated environment.
Expand Down
40 changes: 37 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,31 @@ group = "ai.openclaw"
version = "0.1.0-SNAPSHOT"

repositories {
mavenCentral()
mavenCentral() {
content {
excludeGroup("com.slack.api")
excludeGroup("javax.websocket")
excludeGroup("org.glassfish.tyrus.bundles")
excludeGroup("com.google.code.gson")
}
}
// Ivy repo for deps whose POMs contain DOCTYPE declarations (incompatible with Gradle 9 XML parsing)
ivy {
url = uri("https://repo.maven.apache.org/maven2")
patternLayout {
artifact("[organisation]/[module]/[revision]/[artifact]-[revision].[ext]")
setM2compatible(true)
}
metadataSources {
artifact()
}
content {
includeGroup("com.slack.api")
includeGroup("javax.websocket")
includeGroup("org.glassfish.tyrus.bundles")
includeGroup("com.google.code.gson")
}
}
}

dependencies {
Expand All @@ -17,7 +41,18 @@ dependencies {
implementation("org.java-websocket:Java-WebSocket:1.5.7")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("ch.qos.logback:logback-classic:1.5.3")


// Slack Bolt (Socket Mode) - resolved via Ivy repo (no POM parsing)
implementation("com.slack.api:bolt-socket-mode:1.44.2")
implementation("com.slack.api:slack-api-model:1.44.2")
implementation("com.slack.api:slack-api-client:1.44.2")
implementation("com.slack.api:bolt:1.44.2")
implementation("com.slack.api:slack-app-backend:1.44.2")
implementation("com.slack.api:slack-api-model-kotlin-extension:1.44.2")
implementation("javax.websocket:javax.websocket-api:1.1")
implementation("org.glassfish.tyrus.bundles:tyrus-standalone-client:1.20")
implementation("com.google.code.gson:gson:2.11.0")

testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-core:5.11.0")
Expand Down Expand Up @@ -46,4 +81,3 @@ tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}

29 changes: 29 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash
set -e

# Import any custom CA certs mounted at /certs/*.crt into the JVM truststore.
# Handles both single-cert PEM files and multi-cert PEM bundles.
CACERTS="${JAVA_HOME}/lib/security/cacerts"
if [ -d /certs ] && ls /certs/*.crt 2>/dev/null 1>/dev/null; then
for bundle in /certs/*.crt; do

rm -f /tmp/cert-*.pem
csplit -z -f /tmp/cert- -b '%03d.pem' "$bundle" \
'/-----BEGIN CERTIFICATE-----/' '{*}' 2>/dev/null 1>/dev/null || true
i=0
for pem in /tmp/cert-*.pem; do
[ -f "$pem" ] || continue
alias="imported-$(basename "$bundle" .crt)-$i"
keytool -importcert -noprompt \
-keystore "$CACERTS" \
-storepass changeit \
-alias "$alias" \
-file "$pem" 2>/dev/null || true
rm -f "$pem"
i=$((i+1))
done
done
fi
fi
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Extra fi in entrypoint.sh causes shell syntax error, preventing Docker container startup

The entrypoint.sh script has an unmatched fi on line 27, which causes a bash syntax error that prevents the Docker container from starting.

Root Cause

The shell structure is:

  • Line 7: if [ -d /certs ] && ....; then
  • Line 8: for bundle in /certs/*.crt; do
  • Line 14: for pem in /tmp/cert-*.pem; do
  • Line 24: done (closes inner for pem)
  • Line 25: done (closes outer for bundle)
  • Line 26: fi (closes the if from line 7)
  • Line 27: fiextra/unmatched, causes syntax error

Verified with bash -n entrypoint.sh:

entrypoint.sh: line 27: syntax error near unexpected token `fi'

Impact: The Docker container will fail to start entirely because set -e is enabled and the entrypoint script cannot be parsed. This blocks all Docker-based deployments.

Suggested change
fi
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


exec su-exec openclaw java -Duser.home=/home/openclaw -jar /app/openclaw.jar "$@"
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx512m
Loading
Loading