Dejima

API reference

HTTP + websocket surface for the Dejima daemon. v1 · alpha

The contract for any Dejima client — Scusi, mobile/PWA apps, chat bridges, your own tools. The CLI targets this same API; nothing it does is off-limits to third-party apps.

← Back to landing

Contents

Connecting

The Dejima daemon (dejimad) speaks JSON over HTTP/1.1 and websockets. Three reachable surfaces, all serving the same API:

ScenarioEndpointNotes
Same machine as the daemon ~/.dejima/dejimad.sock (Unix socket) Always on. Auth = filesystem permissions (0600, only the user can connect). Pass to curl with --unix-socket.
Remote machine on the same tailnet http://<host>:7273 Daemon must be started with --tcp :7273 (or via dejima service install). Only accepts connections from Tailscale-assigned IPs. Off-tailnet connections are refused at the listener.
Remote without Tailscale SSH-tunnel the socket ssh -L /tmp/dejimad.sock:/home/you/.dejima/dejimad.sock host, then point your client at the local tunnel.
Into an island (shell / editor) SSH façade — dejimad --ssh <addr> Not the JSON API: the daemon is a single SSH front door (username = island name, per-island public-key auth) that bridges into the container. Powers shell, sftp, and VS Code / Cursor Remote-SSH straight into /workspace. See Remote dev.

The CLI uses DEJIMA_HOST: unset → Unix socket; set to host:port → remote TCP. Apps follow the same convention or pick their own.

curl examples

# Local
curl --unix-socket ~/.dejima/dejimad.sock http://dejimad/v1/healthz

# Remote (on tailnet)
curl http://mac-mini.tailnet.ts.net:7273/v1/healthz

SDKs (Python & TypeScript)

Official thin clients wrap every endpoint below plus the websocket PTY stream — you get the REST surface and attach() without hand-rolling the envelope framing. Both read DEJIMA_HOST / DEJIMA_TOKEN from the environment, raise a typed error on non-2xx, and are generated against openapi.yaml. alpha — 0.x, fields may change until 1.0.

Python — pip install dejima-sdk

pip install dejima-sdk            # REST client
pip install 'dejima[ws]'      # + attach() PTY streams

from dejima import Client

dj = Client()                 # or Client(host="100.84.12.7:7273", token="…")
isl = dj.create_island(repo="git@github.com:you/foo.git", agent="claude-code")
dj.add_agent(isl["name"], type="codex")
print(dj.exec(isl["name"], ["git", "status", "--short"]))

with dj.attach(isl["name"]) as s:     # websocket PTY, multi-attach
    s.send(b"ls -la\n")
    print(s.recv())                   # bytes, or None when closed

TypeScript / JavaScript — npm install dejima

npm install dejima            # Node 18+ (ESM)

import { Client } from "dejima";

const dj = new Client();      // or new Client({ host: "…", token: "…" })
const isl = await dj.createIsland("git@github.com:you/foo.git", { agent: "claude-code" });
await dj.addAgent(isl.name, { type: "codex" });
console.log(await dj.exec(isl.name, ["git", "status", "--short"]));

const s = await dj.attach(isl.name);  // websocket PTY, multi-attach
s.sendText("ls -la\n");
s.onData(bytes => process.stdout.write(bytes));

Source & full coverage list: sdk/python · sdk/ts. Another language? Everything below is one HTTP/WebSocket API — generate a client from the spec and hand-write only the PTY helper.

Authentication

Operator clients (your CLI, dashboard, bot) authenticate by Tailscale identity (remote) or filesystem permissions (local). No per-client tokens, no per-client ACLs — documented honestly:

Trust-on-first-use device allowlisting and optional operator API-token auth are on the roadmap; until they ship, treat the operator API as fully trusted within the tailnet.

In-island tokens (shipped). An agent inside an island reaches the daemon over a separate, default-deny path: the daemon injects a per-island bearer token (DEJIMA_TOKEN) + host (DEJIMA_HOST). That token is scoped to its own island — it can drive the autonomy surface (Port trades, capability execution, child-island spawn that returns the child's token) but is rejected (403) on the control plane and on any other island. This is how a contained "brain" acts without ever holding an operator credential; it's enforced in middleware on the request's escaped path.

Errors

Non-2xx responses carry a JSON body:

{ "error": "island \"foo\" not found" }

HTTP status conventions:

Island endpoints

GET/v1/islandsList all islands

Returns []IslandInfo. Lightweight — no git status; use the per-island detail endpoint for that.

curl http://host:7273/v1/islands
[
  {
    "name": "foo",
    "repo": "git@github.com:you/foo.git",
    "agent": "claude-code",
    "image": "dejima/island:latest",
    "state": "running",
    "container": "running",
    "created_at": "2026-05-21T15:04:05Z",
    "last_used_at": "2026-05-22T10:30:00Z",
    "stats": { "memory_usage_bytes": 245890000, "memory_limit_bytes": 4294967296, "cpu_percent": 12.5 },
    "agent_state": { "latest": "task-complete", "updated_at": "2026-05-22T10:29:32Z" },
    "attached": [ { "label": "laptop", "joined_at": "2026-05-22T10:28:00Z" } ]
  }
]
POST/v1/islandsCreate a new island
POST /v1/islands
{
  "name": "foo",                       // optional; derived from repo if empty
  "repo": "git@github.com:you/foo.git", // required
  "agent": "claude-code",              // optional; default "claude-code"
  "image": "dejima/island:latest",     // optional; default
  "github_identity": "work",           // optional; clone/push as this daemon identity
  "resources": {
    "memory": "4G",                    // optional; --memory passthrough
    "cpus": "2.0",                     // optional; --cpus passthrough
    "disk": "20G"                      // optional; --storage-opt size=
  }
}

→ 201 Created, body is IslandInfo for the new island
→ 409 Conflict if name already exists
GET/v1/islands/{name}Detail view of one island

Like the list response but populated with git status (branch, clean, ahead/behind) computed via container exec. Slower than the list — only call when you need the detail.

DELETE/v1/islands/{name}Purge — destroy container + volumes

Irreversible. Container is stopped + removed, both volumes (workspace + agent state) deleted, per-island network removed, project config dir wiped. Returns 204 No Content.

Lifecycle endpoints

POST/v1/islands/{name}/hibernateStop container, preserve volumes

Sets desired_state = hibernated and stops the container. Workspace and agent on-disk state survive. Returns updated IslandInfo.

POST/v1/islands/{name}/wakeStart a hibernated container

Restarts the container against existing volumes. Sets desired_state = running. If the container was removed (e.g. via docker rm directly), it's recreated against the same volumes. Returns updated IslandInfo.

POST/v1/islands/{name}/resetClear agent state, preserve workspace

Stops container, removes the agent-state volume only, recreates it empty, restarts the container. Workspace and git history are untouched. Use for "fresh conversation, same code."

POST/v1/islands/{name}/upgradeRecreate against a fresher image, keep volumes

Stops + recreates the container on the current island image (rebuilding it first if missing), preserving both volumes and re-assembling all credential mounts. Use after an image change, or to refresh credentials. Returns updated IslandInfo.

POST/v1/islands/{name}/cloneDuplicate an island, volumes and all
POST { "name": "foo-2" }   // the new island's name
→ 201 IslandInfo

Byte-for-byte copies the workspace and home volumes (so credentials/tool-auth come along) into a fresh island; owner/tags/agents carry over. Title and Port grants are deliberately dropped — a clone shows its own name and starts deny-all, never silently inheriting host access.

Sessions (websocket)

GET/v1/islands/{name}/session?label=<client>Multi-attach PTY stream

Upgrades to websocket. Multiple clients can hold this stream concurrently; tmux's native multi-attach gives them all the same screen. The label query param identifies the client in presence; defaults to anonymous.

Wire protocol is JSON frames over the websocket. Server sends:

{"type":"hello",    "attached":[{"label":"laptop","joined_at":"…"}]}    // on connect
{"type":"data",     "b64":"<base64-encoded PTY bytes>"}                // server → client
{"type":"error",    "b64":"<error message>"}                            // recoverable problems

Client sends:

{"type":"data",   "b64":"<base64 stdin bytes>"}     // keystrokes → PTY
{"type":"resize", "rows":24,"cols":80}              // terminal resize

Bytes are base64 because tmux output is raw binary (escape sequences, control bytes) and JSON-over-websocket is the simplest cross-language transport for it.

The bare /session route attaches to the island's primary agent. To target a specific agent, use /v1/islands/{name}/agents/{id}/session (see Agents, below). Presence and shared-screen are scoped per agent.

Agents

An island hosts one or more agents in a single container. Each interactive agent has its own tmux session and git worktree; all agents share the island's home (credentials + tool-auth). A headless agent runs as a supervised process with a per-agent log.

GET/v1/islands/{name}/agentsList the agents in an island
[ { "id": "a1", "type": "claude-code", "tmux": "agent-a1", "branch": "",
    "worktree": "/workspace", "attachable": true, "state": "running" },
  { "id": "a2", "type": "codex", "label": "frontend", "branch": "agent/a2",
    "worktree": "/workspace/.agents/a2", "attachable": true, "state": "running" } ]

Each entry may also carry agent_state (latest agent-emitted signal), attached (clients on that agent), and error (last orchestration failure, if any).

POST/v1/islands/{name}/agentsAdd an agent
POST { "type": "codex", "label": "frontend" }     // interactive
POST { "type": "headless", "cmd": "python loop.py" } // supervised process
→ 201 AgentInfo

The agent gets the next a<N> id, its own worktree/branch (agent/<id>), and — if the island is running — a live session immediately; otherwise it materializes on the next wake. type defaults to the primary agent's type.

GET/v1/islands/{name}/agents/{id}One agent's detail
DELETE/v1/islands/{name}/agents/{id}Remove an agent

Kills the agent's session and prunes its worktree (the branch is kept). The primary and the last remaining agent can't be removed. Returns 204.

GET/v1/islands/{name}/agents/{id}/session?label=<client>Attach to a specific agent

Same websocket protocol as the island /session route above. Interactive agents only — a headless agent returns 409 (use its log instead).

POST/v1/sessions/revokeDrop all active client websockets

Force-disconnects every attached client across every island. Containers and agent processes keep running. Used by dejima logout-all. Returns { "revoked": N }.

GET/v1/clientsRecent attach/detach history

In-memory ring buffer, newest first, bounded at ~200 entries. Lost on daemon restart — by design, this is awareness without surveillance, not a persistent audit log.

[
  { "label": "phone-pwa", "island": "foo", "attached_at": "2026-05-22T10:28:00Z" },
  { "label": "laptop",    "island": "foo", "attached_at": "2026-05-22T10:00:00Z", "detached_at": "2026-05-22T10:27:30Z" }
]

Resources & rename

PUT/v1/islands/{name}/resourcesAdjust memory + OOM priority
PUT { "memory": "6G",        // optional; applied live via `docker update`
      "oom_priority": 3 }    // optional; kernel OOM-kill stack-rank
→ { "resources": { "memory": "6G", "oom_priority": 3 }, "restart_required": true|false }

Memory changes apply live — no restart. OOM priority is set at container create, so changing it returns restart_required: true and takes effect on the next upgrade/recreate. Pointer fields distinguish "unset" from an explicit value.

PATCH/v1/islands/{name}Set the cosmetic display title

Body { "title": "My Cool Project" }. name stays the immutable handle the CLI/API address by; title is display-only (empty → show name). Returns updated IslandInfo.

PATCH/v1/islands/{name}/agents/{id}Relabel an agent

Body { "label": "frontend" }. Cosmetic; the agent id stays the stable handle. Returns updated AgentInfo.

Lightweight access

POST/v1/islands/{name}/execOne-shot command in the island
POST { "cmd": ["ls", "-la", "/workspace"] }
→ { "stdout": "...", "stderr": "...", "exit_code": 0 }

Runs the command, returns its output. No PTY, no streaming — use the session websocket for interactive work.

GET/v1/islands/{name}/files/{path...}Read a file out of the island

Body is the raw file bytes (octet-stream). Used by dejima cp foo:/workspace/path ./.

PUT/v1/islands/{name}/files/{path...}Write a file into the island

Body is the raw file bytes. Parent directories are created if missing. Returns 204 No Content.

GET/v1/islands/{name}/logs?follow=trueStream container / agent logs

Returns a chunked text stream of container stdout + stderr. With ?follow=true, the stream stays open until the client disconnects. Add ?agent=<id> to tail a headless agent's log file instead (interactive agents return 409 — attach to their session).

Remote dev — SSH façade & editors

The daemon can be the single SSH front door to every island (dejimad --ssh <addr>): the username names the island, per-island public-key auth, and the session is bridged into the container via docker exec — no in-island sshd, no published ports, works on macOS + Linux. This is the on-ramp for VS Code / Cursor / any Remote-SSH editor (open the island straight at /workspace) and for sftp; direct-tcpip forwarding is bridged so the editor's in-container server connects.

GET/v1/ssh/account-keysList enrolled SSH public keys
POST/v1/ssh/account-keysEnroll a device public key

Account-wide keys authorize a device against the façade for every island the operator owns. dejima ssh enroll wraps this (key + ~/.ssh/config entries) so onboarding is one command per device.

Credentials & GitHub identities

The daemon holds the agent credentials it injects into islands. Claude creds are seeded from the host; GitHub identities are first-class and per-daemon, so an island clones/pushes as a chosen identity (correct authorship, per-island gh config).

GET/v1/credentials/claudeWhether Claude creds are present
PUT/v1/credentials/claudePush Claude creds to the daemon
GET/v1/credentials/githubList GitHub identities
PUT/v1/credentials/github/{name}Add / update an identity
DELETE/v1/credentials/github/{name}Remove an identity
GET/v1/credentials/github/{name}/reposBrowse an identity's repos (daemon-side)

Daemon-side repo browsing means the create flow works from any device. A github_identity on the island-create request binds the new island to that identity.

Brokered host files (Port)

Port lets an island read — and, once granted :rw, write — specific host folders the operator authorizes, deny-all by default. Every crossing is appended to a hash-chained, tamper-evident ledger. Grants are the operator's act; an in-island token can spend a grant but never widen it.

GET/v1/islands/{name}/port/scopesList an island's host-file grants
POST/v1/islands/{name}/port/scopesGrant a scope (operator)
DELETE/v1/islands/{name}/port/scopes/{scope}Revoke a scope
POST/v1/islands/{name}/port/intakeCopy host files → island (read)
POST/v1/islands/{name}/port/exportCopy island files → a host scope
POST/v1/islands/{name}/port/writeWrite into a granted scope (requires :rw, symlink-safe)
GET/v1/auditRead the ledger (verify with dejima audit --verify)

Capability brokering

A narrow, typed broker so function-calling brains can invoke named host actions — not arbitrary shell: per-island deny-all grants, fixed string→string args, a fixed-schema capability.* ledger. Adapters: macOS Apple Shortcuts, Linux ~/.dejima/capabilities/ scripts.

GET/v1/islands/{name}/capability/grantsList granted capabilities
POST/v1/islands/{name}/capability/grantsGrant a capability (operator)
DELETE/v1/islands/{name}/capability/grants/{target}Revoke
POST/v1/capabilities/executeRun a granted capability (in-island token)
POST { "target": "send-imessage", "args": { "to": "...", "body": "..." } }

Reachable by an island's own token only, against a granted target; deny-all and ledgered. No shell, no free-form commands.

MCP brokering

The same deny-all, ledgered pattern for Model Context Protocol servers. The operator curates host-side MCP servers; an island can call one only once it's been granted, and only through a fixed method allow-list (tools/list, tools/call, resources/*, prompts/*). The broker execs the registered server with fixed argv — never via a shell — speaks JSON-RPC over stdio, and records every grant, revoke, call, and denial in the mcp.* ledger (params SHA-256-hashed). CLI: dejima mcp grant|revoke|list|call.

GET/v1/islands/{name}/mcp/grantsList an island's granted MCP servers
POST/v1/islands/{name}/mcp/grantsGrant a server (operator)
DELETE/v1/islands/{name}/mcp/grants/{server}Revoke a grant
POST/v1/mcp/callOne brokered JSON-RPC call against a granted server
POST { "island": "web", "server": "files", "method": "tools/call",
       "params": { "name": "fetch", "arguments": { "url": "..." } } }
// → { "ok": true, "is_error": false, "result": {...}, "ledger_seq": 1024 }

Grants are the operator's act; an in-island token can spend a grant but never widen it. Disallowed methods, oversized payloads, and ungranted servers are rejected (400/403) and the denial is ledgered.

Host terminals

Uncontained operator shells in tmux on the daemon host (humans, not agents — an agent is always a container). Gated behind dejimad --host-terminals (off by default), operator-only, island tokens denied. Mirrors the dejima term CLI / TUI Host section.

GET/v1/terminalsList host terminals
POST/v1/terminalsCreate one
GET/v1/terminals/{id}/sessionAttach (websocket PTY)
PATCH/v1/terminals/{id}Relabel
DELETE/v1/terminals/{id}Close

Safety switch (panic)

Stop everything fast and keep it stopped. POST writes a ~/.dejima/PANIC flag and stops every island; the daemon refuses to auto-start anything while it's set (survives a daemon restart). Emits daemon.panic-engaged / daemon.panic-cleared.

GET/v1/panicPanic status
POST/v1/panicEngage — stop all, set the flag
DELETE/v1/panicClear — restart islands meant to be running

Operator: self-update

POST/v1/admin/updateUpdate the daemon (source or release)
POST { "execute": false }   // dry-run: reports the plan
→ { "current": "...", "latest": "...", "mode": "source|release",
    "update_available": true, "applying": false }

Operator-only (island tokens can't reach it). With execute: true the apply runs synchronously (so real failures surface to the caller), then the daemon restarts asynchronously; islands survive via adopt-existing.

Events & webhooks

Dejima emits typed events on lifecycle changes, client attach/detach, and (when an agent shim is installed) agent activity. Two ways to consume them:

GET/v1/events/subscriptionsList webhook subscriptions
POST/v1/events/subscribeSubscribe a webhook URL
POST { "url": "https://your-app.example.com/dejima",
       "secret": "optional-hmac-key",
       "events": ["island.hibernated", "agent.waiting-for-input"]   // optional filter
     }
→ 201 Created, body is { "id": "...", "url": "...", "created_at": "..." }
DELETE/v1/events/subscriptions/{id}Remove a webhook
GET/v1/islands/{name}/eventsRecent events for one island

Newest first, bounded at ~50 events. In-memory, lost on daemon restart.

Webhook delivery format

Daemon POSTs JSON to each subscribed URL on every matching event:

POST <your-url>
Content-Type: application/json
X-Dejima-Event: island.hibernated
X-Dejima-Signature: sha256=<hex hmac of body>  (only if you provided a secret)

{
  "type": "island.hibernated",
  "island": "foo",
  "timestamp": "2026-05-22T10:30:00Z",
  "payload": { ... event-specific fields ... }
}

Delivery is best-effort, fire-and-forget; the daemon doesn't retry. Non-2xx responses are logged but don't block. If you need durability, queue from your own HTTP handler.

Server overview

GET/v1/overviewServer-wide totals + substrate health
{
  "total_islands": 4,
  "running": 3,
  "hibernated": 1,
  "errored": 0,
  "attached_clients": 1,
  "memory_usage_bytes": 1234567890,
  "memory_limit_bytes": 8589934592,
  "cpu_percent": 14.2,
  "daemon_started_at": "2026-05-22T08:00:00Z",
  "webhook_count": 1,
  "docker_reachable": true,
  "island_image_present": true,
  "island_image": "dejima/island:latest",
  "host_memory_bytes": 25769803776,
  "vm_memory_bytes": 18790481920,
  "vm_recommended_bytes": 19327352832
}

Used by the TUI's footer health strip and dejima overview. The two *_reachable/*_present booleans are a cheap "is the substrate healthy?" probe — clients can poll this every few seconds and surface a red badge when either turns false.

The substrate-memory triplet diagnoses the most common cause of island OOMs: on macOS, Docker runs a Linux VM (colima/Docker Desktop) given a fixed slice of host RAM, and that slice is the ceiling all islands share. host_memory_bytes is the daemon host's physical RAM; vm_memory_bytes is the runtime's memory ceiling (the VM total — measured as the largest per-container limit across running islands); vm_recommended_bytes is the size to suggest (¾·host, leaving the host ≥4 GiB). All three are omitted when undeterminable. When vm_memory_bytes is well under vm_recommended_bytes, the TUI shows a resize banner and dejima doctor --fix offers the colima resize.

GET/v1/healthzLiveness

{ "status": "ok" } when the daemon is reachable. Use to check whether dejimad is up before issuing real calls.

Agent-event endpoint (shim-only)

POST/v1/internal/agent-eventUsed by per-agent shims inside islands

Per-agent shim scripts (currently Claude Code's hook integration) POST agent-specific events here to surface them through the rest of the system. Not meant for external clients — it has no auth beyond access to the daemon socket, and is reachable from inside islands via the bind-mounted socket at /run/dejima/dejimad.sock.

POST { "island": "foo",
       "agent": "a2",                  // optional; which agent (from DEJIMA_AGENT_ID)
       "type": "agent.waiting-for-input",
       "payload": { "tool": "..." }    // optional, free-form
     }
→ 202 Accepted

The event propagates to webhook subscribers and to the per-island recent-events feed exactly like any other event.

Data types

IslandInfo

FieldTypeNotes
namestringUnique handle. ^[a-z0-9][a-z0-9._-]{0,62}$
repostringGit URL or local path used at init
agentstringclaude-code | codex | custom
imagestringDocker image, e.g. dejima/island:latest
statestringDesired state from config: running or hibernated
containerstringObserved state: running, exited, missing, errored
created_atRFC3339
last_used_atRFC3339
attached[]PresenceEntryLive client list; empty if no one is connected
statsIslandStats?Memory + CPU; nil if not running
agent_stateAgentStateInfo?Latest agent-emitted event (island rollup)
agents[]AgentInfoThe agents in the island (≥1); agent mirrors the primary's type
gitGitInfo?Detail endpoint only (slow path)
titlestringCosmetic display name; empty → show name
rolestring"" (work island) or home (assistant Home Island)
owner / tagsstring / mapCreator label + free-form tags (for rollups)
resourcesResources?Configured caps + OOM priority; detail endpoint
healthIslandHealth?oom_killed, restart_count, exit_code; detail endpoint
diskIslandDisk?Workspace + home volume sizes; detail endpoint

AgentInfo

FieldTypeNotes
idstringStable per-island handle: a1, a2, …
typestringclaude-code | codex | headless | custom
labelstringOptional, user-set
branch / worktreestringThe agent's git branch + working directory
attachableboolFalse for headless (no PTY)
statestringrunning / stopped / exited (session alive but the agent process died) — detail/list only
restartsintTimes a supervised headless agent crash-looped; a climbing count ⇒ likely OOM
agent_stateAgentStateInfo?This agent's latest emitted signal
attached[]PresenceEntryClients on this agent
errorstringLast orchestration failure, if any

IslandStats

FieldType
memory_usage_bytesuint64
memory_limit_bytesuint64 (0 = unlimited)
cpu_percentfloat64

Resources

FieldTypeNotes
memorystringe.g. 4G; live-updatable via the resources endpoint
cpusstringe.g. 2.0
diskstringe.g. 20G
oom_priorityint?OOM-kill stack-rank; nil = smart default (set at create)

AgentStateInfo

FieldType
lateststring — waiting-for-input, task-complete, error
updated_atRFC3339

GitInfo

FieldType
branchstring
cleanbool
aheadint
behindint
dirty_filesint

PresenceEntry

FieldType
labelstring — e.g. laptop, phone-pwa
joined_atRFC3339

Event types

Daemon-observable (always emitted)

TypeFires when…
island.createdA new island has been provisioned
island.runningThe container has transitioned to running
island.hibernatedContainer stopped, volumes preserved
island.wokenContainer restarted from hibernation
island.resetAgent state cleared, workspace preserved
island.purgedContainer + volumes destroyed
container.crashedContainer exited unexpectedly (roadmap — once the watchdog ships)
client.attachedA websocket client has joined a session — this is your "someone connected" signal
client.detachedA websocket client has left
last-client.detachedAll clients have left an island; nobody is watching anymore

Agent-emitted (opt-in via per-agent shim)

These fire only for agents whose shim is wired up (Claude Code today; Codex shim does not yet emit these but is roadmap'd).

TypeFires when…
agent.waiting-for-inputThe agent is paused awaiting human input
agent.task-completeThe agent finished a unit of work
agent.errorThe agent hit an error or refused to continue

End-to-end example

Suppose you're building a Slack bot that drives Dejima. The shape:

1. Subscribe to events

curl http://mac-mini:7273/v1/events/subscribe \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-bot.example.com/dejima","secret":"hush"}'

2. Receive an event in your bot

POST https://your-bot.example.com/dejima
X-Dejima-Event: agent.waiting-for-input
X-Dejima-Signature: sha256=<hmac>

{ "type": "agent.waiting-for-input", "island": "foo", "timestamp": "…" }

Your bot posts a Slack message: "foo is waiting for your input".

3. User replies in Slack

Your bot opens the session websocket and sends the user's text:

// Pseudocode
ws = connect("ws://mac-mini:7273/v1/islands/foo/session?label=slack-bot")
ws.send({ type: "data", b64: base64("Yes, proceed with the refactor.\n") })

The agent receives the input through tmux, the websocket streams the agent's reply back, your bot posts it as Slack messages, and the cycle continues.

4. User wants to see the screen

Your bot can attach as a passive observer (same endpoint, different label):

ws2 = connect("ws://mac-mini:7273/v1/islands/foo/session?label=slack-readonly")
// Don't send any data frames — just decode the b64 stream into a rendered terminal screenshot.

Stability

The API is v1/-prefixed and intended to be stable, but Dejima itself is in alpha — breaking changes may happen if real-world dogfood surfaces issues. We'll version-bump (v2/) rather than break v1/ silently. The single source of truth is the source code; this page is hand-maintained and may drift slightly between releases.

Links