Skip to content

Instantly share code, notes, and snippets.

@jfarcand
Created April 23, 2026 21:11
Show Gist options
  • Select an option

  • Save jfarcand/9a1722622086e6982f0e485027128493 to your computer and use it in GitHub Desktop.

Select an option

Save jfarcand/9a1722622086e6982f0e485027128493 to your computer and use it in GitHub Desktop.
Atmosphere OpenClaw workspace bridge — architecture + wiring

The OpenClaw Bridge — Full Walkthrough

Why it exists

OpenClaw is a canonical, filesystem-as-source-of-truth workspace convention for AI agents: you author a directory of Markdown files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md, MEMORY.md, memory/YYYY-MM-DD.md, skills/*) and the runtime boots an agent directly from that directory. No YAML manifest, no code, no custom DSL.

The design goal locked in docs/foundation-primitives.md and CHANGELOG.md: an OpenClaw workspace authored without any Atmosphere extensions must run on Atmosphere without conversion. That zero-config promise is the reason the bridge exists. The alternative — "bring your OpenClaw agent, first translate it to an Atmosphere YAML" — would make Atmosphere yet another proprietary format and kill portability.

Where it lives

modules/ai/src/main/java/org/atmosphere/ai/workspace/
  AgentWorkspace.java                   ← SPI (interface)
  AgentDefinition.java                  ← record returned by the SPI
  AgentWorkspaceLoader.java             ← ServiceLoader-based dispatcher
  OpenClawWorkspaceAdapter.java         ← the bridge itself
  AtmosphereNativeWorkspaceAdapter.java ← fallback when there's no convention
  package-info.java
META-INF/services/org.atmosphere.ai.workspace.AgentWorkspace
  (registers OpenClawWorkspaceAdapter + AtmosphereNativeWorkspaceAdapter)

Tests: AgentWorkspaceLoaderTest, OpenClawCompatTest, plus a real-shape fixture at modules/ai/src/test/resources/fixtures/openclaw/ that mirrors the canonical layout.

Consumer: modules/agent/src/main/java/org/atmosphere/agent/processor/AgentProcessor.java:392-400 — this is where the AgentProcessor wires an AgentWorkspace into the per-agent injectable scope at startup. The sample samples/spring-boot-personal-assistant/src/main/resources/agent-workspace/ is the demo payload.

The SPI shape

AgentWorkspace is a four-method interface (AgentWorkspace.java:53):

boolean supports(Path workspaceRoot);      // claim-check
AgentDefinition load(Path workspaceRoot);  // parse
String name();                             // stable id, for logging/admin
default int priority() { return 100; }     // lower runs first

The bridge owes the loader nothing beyond those four methods. Third parties (Claude Code, Cursor, a bespoke convention) register their own adapter through META-INF/services/ and the loader picks it up without any code change in core.

Output type AgentDefinition is a record that surfaces the raw bootstrap files both individually (so an admin UI can render or edit each section separately) and composed as a single systemPrompt ready to prefix every conversation turn. It also carries:

  • skillPaths — absolute paths to discovered SKILL.md files in precedence order
  • atmosphereExtensions — a Map<String, String> of Atmosphere-only extension file names → raw Markdown, for downstream primitives to parse according to their own schemas

What OpenClawWorkspaceAdapter actually does

1. Layout recognition (supports)

supports() returns true iff the directory contains an AGENTS.md at its root. That's the minimum OpenClaw marker; everything else is best-effort.

return Files.exists(workspaceRoot.resolve("AGENTS.md"));

No partial match, no heuristic. One file decides. A directory without AGENTS.md gets declined — the native fallback catches it.

2. Reading the canonical files (load)

The adapter normalizes the root to absolute+normalized, then reads these four canonical files into the AgentDefinition:

File Semantic slot Required?
AGENTS.md operating rules yes (via supports)
SOUL.md persona no
USER.md user profile no
IDENTITY.md agent identity no

Missing files become empty strings — no errors, no placeholders.

3. Composing the system prompt

composeSystemPrompt() stitches the non-blank sections into a single Markdown block:

## Identity

<IDENTITY.md body>

## Persona

<SOUL.md body>

## User

<USER.md body>

## Operating rules

<AGENTS.md body>

Order is fixed. The pipeline injects this whole string as the system message on every conversation turn — that's the mechanical contract.

4. Atmosphere-only extensions

Five filenames are read separately and stashed in atmosphereExtensions:

ATMO_EXTENSIONS = List.of(
    "CHANNELS.md", "MCP.md", "RUNTIME.md",
    "PERMISSIONS.md", "SKILLS.md");

These are not part of OpenClaw. OpenClaw-native tooling ignores them. Atmosphere reads them and hands them to downstream primitives (CHANNELS.mdChannelRegistry, MCP.mdToolExtensibilityPoint, PERMISSIONS.mdAgentIdentity, etc.). This is the "extend without forking" story: drop these files into any OpenClaw workspace; OpenClaw still runs it, Atmosphere picks up the extras.

Critically, MEMORY.md is not surfaced through atmosphereExtensions even though OpenClaw uses it — memory is read by FileSystemAgentState at a different layer. The compat test asserts this explicitly (OpenClawCompatTest.java:92-93). Preventing memory from bleeding into the extension map keeps concerns separated.

5. Skill discovery

discoverSkills() walks two directories one level deep:

<workspace>/skills/<skill-name>/SKILL.md
<workspace>/.agents/skills/<skill-name>/SKILL.md

Workspace skills come first, .agents/skills second — precedence order is documented in AgentDefinition.skillPaths.

6. Name inference

inferName() prefers an explicit name: marker at the top of IDENTITY.md so the workspace directory can be renamed without changing the agent's identity:

# IDENTITY.md
name: pierre
vibe: focused, minimalist

Falls back to the directory name if no name: marker is present (AgentWorkspaceLoaderTest.identityNameFallsBackToDirectoryName pins that behavior).

How the loader picks an adapter

AgentWorkspaceLoader:

  1. Discovers adapters via ServiceLoader.load(AgentWorkspace.class).
  2. Defensively adds OpenClawWorkspaceAdapter + AtmosphereNativeWorkspaceAdapter if either is missing (defense against shaded deployments that drop META-INF/services/).
  3. Sorts by priority(): OpenClaw = 10, Native = Integer.MAX_VALUE.
  4. At load() time, polls adapters in priority order, picks the first whose supports() says yes.

Result:

  • AGENTS.md present → OpenClaw wins (priority 10).
  • No AGENTS.md → falls through to native (AtmosphereNativeWorkspaceAdapter), which accepts any directory and reads README.md as operating rules if present.
  • No adapter in the list → IllegalStateException with a clear message.

loaderPicksOpenClawOverNative and loaderFallsBackToNativeWhenNoSpecificMatch tests lock the selection algorithm.

How it's wired at runtime

AgentProcessor.buildFoundationPrimitives() (modules/agent/src/main/java/org/atmosphere/agent/processor/AgentProcessor.java:392) is the one consumer today:

var loader = new org.atmosphere.ai.workspace.AgentWorkspaceLoader();
var adapters = loader.adapters();
if (!adapters.isEmpty()) {
    injectables.put(AgentWorkspace.class, adapters.get(0));
}

The workspace root itself is resolved (same method, lines 363-372) from:

  1. -Datmosphere.workspace.root=<path> system property
  2. ATMOSPHERE_WORKSPACE_ROOT env var
  3. Default: ~/.atmosphere/workspace/agents/<agentName>/

Same shape as OpenClaw's convention by design. Tests override with the system property.

Injected into the per-agent scope means any @AiTool method, @Prompt handler, or coordinator step can declare AgentWorkspace as a parameter and receive the live adapter without a ThreadLocal shim — this is Atmosphere's standard injection model.

Compatibility tests — the "without conversion" proof

OpenClawCompatTest.openClawWorkspaceLoadsIdenticallyOnAtmosphere is the closest thing we have to an end-to-end portability assertion. It:

  1. Copies a real OpenClaw-shaped fixture (fixtures/openclaw/) with AGENTS.md, SOUL.md, USER.md, IDENTITY.md (with name: pierre), MEMORY.md, memory/2026-04-15.md, skills/example-skill/SKILL.md, plus CHANNELS.md + MCP.md Atmosphere extensions.
  2. Loads via the real AgentWorkspaceLoader + OpenClawWorkspaceAdapter.
  3. Asserts the OpenClaw adapter wins (def.adapterName() == "openclaw"), name comes from IDENTITY.md, all four sections show up in the composed prompt, the two extension files appear in atmosphereExtensions, MEMORY.md and AGENTS.md do not appear there, and the example-skill is discovered.

The companion openClawDirectoryWithoutAgentsMdIsRejected asserts the negative: a directory with SOUL.md but no AGENTS.md must NOT be claimed by the OpenClaw adapter — otherwise the fallback would never get its turn on ambiguous directories.

Limits / honest scope

  • One workspace per load call. No multi-workspace composition in the adapter itself. If you need that, stack AgentWorkspaceLoader calls and merge AgentDefinitions at the call site.
  • No write-back. Adapters are read-only. FileSystemAgentState is the thing that writes (memory promotions, session JSONL). Keeping writes out of the adapter keeps the SPI small and testable.
  • Extensions are raw Markdown. The bridge does NOT parse CHANNELS.md/MCP.md/etc. — it hands the raw bytes to downstream primitives, which have their own schemas. That's intentional: one parser per concern, not a mega-parser.
  • Classpath-only registration at the moment. The loader is new AgentWorkspaceLoader() per-boot; there's no admin hot-reload endpoint yet. If you want a new adapter live, you restart.
  • .agents/skills/ walk is one level deep. Nested skill trees (skills/team-a/coder/SKILL.md) aren't discovered today. That's a followup if we want it.

Why this shape, not another

The alternatives we rejected are documented in docs/foundation-primitives.md:159:

  • Custom Atmosphere YAML manifest — the OpenClaw workspace IS the manifest; another format would have been a portability downgrade.
  • Annotation-only agents (@Agent("my-pa") class PrimaryAssistant {}) — that exists (@AiEndpoint, @Agent) but ties identity/persona/rules to Java source; the workspace bridge lets non-developers author agents.
  • Database-backed agent store — violates the ".agent is a directory" invariant that makes git diff the primary edit UX.

The bridge is the tightest translation layer that makes OpenClaw + Atmosphere coexist without either side knowing about the other. That's the entire trick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment