# openclaw-joaxclaw-fs

Gateway plugin that exposes JoaxClaw's host-side **teams**, **processes**, and **local
LLM engine** checks over the WebSocket so they work on a **remote** gateway (and so
agent-authored teams/processes are reachable from the app).

They live as files in the gateway host's state dir:

```
<stateDir>/teams/<id>.team.json       TeamBlueprint (source of truth)
<stateDir>/teams/<id>.md              compiled ProcessDef
<stateDir>/teams/<id>.revisions.json  revision snapshots
<stateDir>/processes/<id>.md          process definition
<stateDir>/processes/.runs/<id>.json  persisted run state
```

The desktop app normally reads these via local Electron file IPC, which only reaches
the host when the gateway is local. This plugin registers `teams.*` / `processes.*`
gateway RPC methods that read/write those directories on the host.

## Methods

| Method | Scope | Params | Returns |
|---|---|---|---|
| `teams.list` | read | — | `{ teams: [{ id, blueprint, md, revisions }] }` |
| `teams.get` | read | `{ id }` | `{ id, blueprint, md, revisions }` |
| `teams.set` | write | `{ id, blueprint?, md?, revisions? }` | `{ ok, id }` |
| `teams.run` | write | `{ id, task, autorun? }` | `{ ok, id, nonce }` |
| `teams.delete` | write | `{ id }` | `{ ok, id }` |
| `processes.list` | read | — | `{ defs: [{ id, md }], runs: [{ id, run }] }` |
| `processes.get` | read | `{ id }` | `{ id, md }` |
| `processes.set` | write | `{ id, md }` | `{ ok, id }` |
| `processes.delete` | write | `{ id }` | `{ ok, id }` |
| `processes.runs.set` | write | `{ id, run }` | `{ ok, id }` |
| `engines.probe` | read | `{ url, timeoutMs? }` | `{ ok, status }` |
| `engines.fetch` | read | `{ url, timeoutMs? }` | `{ ok, status, body }` |
| `engines.pull` | write | `{ baseUrl, model }` | `{ pullId }` (Ollama; streamed, **overall** %) |
| `engines.pullStatus` | read | `{ pullId }` | `{ status, completed, total, done, error?, model }` |
| `engines.delete` | write | `{ baseUrl, model }` | `{ ok, status }` (Ollama) |
| `engines.show` | read | `{ baseUrl, model }` | `{ ok, status, body }` (Ollama `/api/show`) |
| `engines.keepAlive` | write | `{ baseUrl, model, keepAlive }` | `{ ok, status }` (load `<0` / unload `0`) |
| `memory.status` | read | — | `{ ok, feature: 'memory-skills' }` (presence probe) |
| `memory.skill.set` | write | `{ slug, markdown }` | `{ ok, slug }` (writes `skills/<slug>/SKILL.md`) |
| `memory.skill.remove` | write | `{ slug }` | `{ ok, slug }` |
| `memory.list` | read | `{ providerId, config }` | `{ items: [{ id, title, subtitle? }] }` (host-side browse) |
| `memory.read` | read | `{ providerId, config, id }` | `{ content }` |
| `memory.graph` | read | `{ providerId, config }` | `{ graph: { nodes, edges } }` (Obsidian backlink graph) |
| `host.metrics` | read | — | `{ ok, cpu, ramUsed, ramTotal, gpu: [{ model, utilizationGpu, memUsed, memTotal, temperatureGpu }] }` (gateway host CPU %/RAM bytes/GPU MB) |
| `host.files.roots` | read | — | `{ roots: [{ id, label, path, agentId? }] }` (listable dirs: workspaces + media) |
| `host.files.list` | read | `{ root, subdir? }` | `{ entries: [{ name, path, size, mtimeMs, isDir }], dir, truncated }` |
| `host.files.read` | read | `{ path, encoding?, offset?, length? }` | `{ path, size, mediaType, encoding, content, eof }` (chunked) |
| `jobs.list` | read | — | `{ jobs: [{ id, command, running, done, exitCode, percent, startedAt, elapsedMs, sessionKey }] }` (background script jobs) |
| `jobs.get` | read | `{ jobId }` | `{ id, command, running, done, exitCode, error, percent, elapsedMs, output, outputTruncated, sessionKey }` (live tail) |
| `jobs.stop` | write | `{ jobId }` | `{ ok }` (SIGTERM the job) |

Agent tools `script_start` / `script_status` / `script_stop` let the model launch a
long-running script in the background (returns a `jobId`), poll it, or stop it — so the
turn doesn't block and JoaxClaw can show a live progress card. `jobs.*` are the app-side
readers for that card.

Artifacts are passed through verbatim as strings (or `null` when missing); the app
owns (de)serialization. Ids are validated to stay inside the state directories.
`memory.skill.*` lets the Memory tab manage a memory connection's agent skill on a
remote gateway host (the local `~/.openclaw` is the wrong machine there); `memory.list`
/`memory.read` browse a server-local store's content. A credential in `config` may be an
`env:VAR` reference, resolved from the host's environment so the secret stays out of the
config and the skill file.

`teams.run` lets an agent launch a saved team against a concrete `task`. It's thin by
design: it only records the request as `<id>.runrequest.json` (with a one-shot `nonce`);
the JoaxClaw app polls for it, builds the launch prompt, optionally auto-launches when
`autorun` is set, and clears the request. The app owns prompt compilation and the live
run monitor, so the plugin never starts a run itself.

`host.files.*` back the app's **Files** panel — the documents agents write live on the
gateway host, so on a remote gateway they're otherwise unreachable. **Read-only:** the
models author these files, the app views them. Listing is confined to roots the plugin
computes itself (`<stateDir>/workspace`, each `<stateDir>/agents/<id>/workspace`, and
`<stateDir>/media`) — the client picks a root by id and can only walk downward, so it
can't aim the listing at an arbitrary directory; dotfiles are skipped and a symlink
pointing out of its root is dropped. `host.files.read` does accept any absolute path,
because the app opens files an agent named in chat that legitimately sit outside those
roots (a repo, `/tmp`) — and `host.readMedia` has always read arbitrary paths, so
restricting it here would break the feature without removing the capability. What it
adds is a denylist: credential files (`openclaw.json`, `credentials*`, `.env`, `*.pem`,
`*.key`, ssh/gnupg/aws/gcloud trees) are never readable through this API. Reads are
chunked (`offset`/`length`, 4 MiB per call, `eof` tells the app whether more remains),
which is what lets the viewer preview a large file and Save As pull the whole thing.

`engines.probe` / `engines.fetch` GET a local LLM engine's health/model URL **from
the gateway host** — that's how the app checks liveness and lists models for engines
on the host's loopback/LAN (unreachable from a remote client). The requested URL is
guarded to local-engine hosts only (loopback, `*.local`, private IPv4); anything else
is rejected, so these can't be used as a general request proxy. `engines.fetch` caps
the returned body at 1 MiB; the app parses it (Ollama `/api/tags`, OpenAI `/models`).

## Install

From npm (recommended — works on any gateway host with internet). `--force` makes
this same command upgrade an existing install:

```bash
openclaw plugins install --force openclaw-joaxclaw-fs
openclaw plugins enable joaxclaw-fs
openclaw plugins inspect joaxclaw-fs   # verify it registered (exits non-zero if not)
openclaw gateway restart
```

Local gateway (dev — symlink so edits reload):

```bash
openclaw plugins install --link ./plugins/joaxclaw-fs
openclaw gateway restart
```

Remote gateway, no shell access: JoaxClaw's **Install via agent** flow (on the remote
Teams or Processes screen) installs this on the host for you (it embeds the plugin
files in a chat message, so it works even air-gapped). The app uses these methods
automatically when the plugin is present.

### Publishing

CI publishes this package to npm automatically when its `package.json` version
changes on `main` (`.github/workflows/publish-plugin.yml`). Bump the `version`, update
this README / the app's CHANGELOG, and push — the workflow is a no-op for any version
already on npm. Requires an `NPM_TOKEN` repository secret with publish rights.
