Repository navigation
feat(openclaw): add OpenClaw as an alternative agent runtime - #35
Open
jannikluhn wants to merge 10 commits into
Open
jannikluhn wants to merge 10 commits into
jannikluhn wants to merge 10 commits into
Conversation
A Session name's constraint comes from what the name becomes in a Runtime, a file path under pi and a session key under OpenClaw, so there is no framework-wide grammar. A Runtime fails a Run whose name it cannot address and accepts the shape the Worker mints for a fresh Session. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Signal Worker names a fresh Session run_<uuid> and every Runtime must accept that shape; the Runtime contract now says so. The pi Runtime's doc comment presents its name grammar as its own, with the reason: the name becomes a file under sessionsDir. A test hands the Worker's mint shape through run against the scripted RPC server and proves it is not refused. The editing guide's note on re-reading pi's session id check says why the grammar is pi's alone. No OpenClaw code and no change to the regex. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… back A second Runtime, createOpenClawRuntime on the /openclaw subpath, takes the daemon URL, the shared token, one Ed25519 private KeyObject, an optional agent id and an optional logger. Each Run opens one connection through the official client package, signs the daemon's challenge with hooks from node:crypto, subscribes to the Session, sends the Prompt with deliver false and follows the chat frames for its own runId to the first final, error or aborted one, then closes the connection. The Session name is passed through verbatim; the daemon prefixes it. Construction refuses a key that is not an Ed25519 private key. The scopes requested are operator.read and operator.write, because a token-only connection is accepted and then refused every method. Every connection receives every Run's frames, so a frame counts only by its runId. Test support gains a scripted daemon: an in-process WebSocket server that plays the challenge, verifies the device signature the way the daemon does, answers connect, the subscription and chat.send, numbers its events, records every request and can be told to refuse or hang up. The two OpenClaw packages are regular dependencies with caret ranges, confined to src/openclaw by the one Biome overrides entry, proven by the import-confinement test, the package check and the reference documentation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ailure The OpenClaw Runtime's outcome now carries one UsageReport per model call the Run made, read from the session.message transcript records the daemon streams to the connection that subscribed to the Session before chat.send. A record belongs to the Run by its __openclaw.runId and never by order, since the connection hears every Run in the Session. The records collected until the Run's first terminal frame are its Usage, whether that frame was final, error or aborted, or the connection dropped; every record of a Run arrived before its terminal frame in every observed Run, so nothing waits after it. Provider, model and the four token counts are passed on as sent, cost is the daemon's cost.total, and totalTokens is summed from the four counts as the type promises. The daemon's all-zero record for a refused or cut-off call is kept as a call that charged nothing. The session-level usage method is never called, and UsageReport is unchanged. The reading lives in src/openclaw/usage.ts, one function from a payload and a run id to a report or nothing. The scripted daemon gains the observed session.message shape: modelCall builds one assistant record, modelCalls plays a Run's calls in the observed order with the Prompt's user record and the agent usage frames between, refusedCall is the daemon's zero record, and the daemon stamps the Run's id into __openclaw and the session key beside the message as it does into chat and agent frames. The tests drive run against the scripted daemon for a three-call Run, a Run refused after two calls, an abort, a dropped connection, another Run's records and the session-level method, and one test runs a Signal through createSignalWorker with this Runtime and reads the saved Usage back through the Usage Component, as a pi Run's reads back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…not act on The OpenClaw Runtime now carries a Session name grammar of its own, in src/openclaw/session-names.ts, decided from what daemon 2026.8.1 was observed to do with a key rather than transcribed from a rule of its own. Refused, with the Run failing before any connection and the message naming the rule: a blank or whitespace-only name, one over 512 characters, any colon (the daemon's key segment separator, which keeps the agent:, subagent:, cron: and acp: forms out of a Handler's reach), the names main and global (the agent's main Session, into which the daemon folds global), any uppercase letter (the daemon lowercases every key, so User_42 and user_42 would be one Session) and leading or trailing whitespace (the daemon trims it). Dots, dashes, underscores, slashes, inner spaces, non-ASCII and the incognito- prefix pass through verbatim, as does the run_<uuid> the Signal Worker mints. The Runtime's doc comment states each refusal and its reason, including that the daemon lowercases. A usageLimit that is not a positive whole number fails the Run before any connection, worded as under pi. A connect refused with details.code PAIRING_REQUIRED fails the Run with the pairing request id and the command that approves it on the daemon host, openclaw devices approve <requestId>, in place of the client's bare message. The new cases are proven in src/openclaw/failures.test.ts against the scripted daemon: eighteen refused names and eleven accepted ones, the five invalid limits pi refuses, and the pairing refusal in the frame shape the prototype recorded, with the connection closed and nothing sent after it. Every case is an ok: false outcome and no test observes a throw from run. The one existing fixture with an uppercase letter is lowercased, and the package check lists the new shipped module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The one test that talks to the real thing: the pinned OpenClaw daemon (ghcr.io/openclaw/openclaw:2026.8.1, named once in src/test-support/docker.ts) runs in Docker with the existing scripted model as its only provider, so no model credential is needed. The first Run from a fresh Ed25519 key is refused with the Runtime's pairing message, the request id is read out of that message and approved with `devices approve` inside the container, the next Run returns ok with the usage the model's counts imply, and a second Prompt into the same Session reaches the model with the first Prompt and the first answer in view. The container runs as the image's own user, since the daemon dies under `--user <host uid>` creating /home/node/.cache; the state directory is opened to it instead. It is skipped unless CONCORDE_CONTAINER_TESTS opts in and runs from `npm run test:container` beside the pi test, in about eight seconds. Also lists dist/openclaw/usage.js and .d.ts among the shipped files, which the usage module of the previous change left out of check-package. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each session.message record of the Run is added to its total as it arrives, and the record that takes the total past the Prompt's usageLimit sends chat.abort with the session key, the configured agent and the run id. The Run is followed to its terminal frame and fails with limited: true and every report collected until then, in pi's wording. The daemon starts no further call once it accepts the abort and the call in flight never reports usage, so the overshoot is that one call and a limited Run's recorded Usage can sit below the cap; the doc comment says so. An aborted frame the Runtime did not cause stays an ordinary failure, and a Run that finished or failed before the abort landed is limited with a message saying which. The fake daemon gains an abortable scripted Run, held open per call and ended by chat.abort with the frames Finding 5 recorded, and the scripted model a tool-call reply, so the container test runs a limited two-call Run against the real daemon. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The glossary's Agent server entry no longer calls the Agent Instance's address unauthenticated as a rule: that is pi's RPC, which has no authentication of any kind, while the OpenClaw daemon's address is authenticated by a signed challenge with a device key the Operator provisions and approves once, the daemon's shared token and scopes. The Agent Instance entry reaches "the address it accepts connections on" rather than an RPC channel, and the Usage limit entry says that whether the stopped call's tokens are in the outcome is the Agent Implementation's doing, pi reporting the partial call and the OpenClaw daemon nothing. The architecture document gains a section on running OpenClaw as the agent implementation: the one daemon process every run shares against pi's listener that forks per connection, the authenticated address, what the operator provisions and the one-time approval on the daemon host, the plaintext ws:// rule, delivery being off so the messenger stays the only path to a user, the session name as the last segment of the daemon's key and the daemon's lowercasing as the reason uppercase names are refused, and where usage and the usage limit differ from pi. The sentences calling the agent instance's RPC listener unauthenticated, in the architecture document, the README and the site index, now hold for both; the README's runtime paragraph names both runtimes. The editing guide gains the silent failures this feature made: the OpenClaw Runtime's own Session grammar going stale against a newer daemon, deliver: false on every chat.send, the host hooks the client package leaves to its embedder and the identity hook whose undefined default makes a remote daemon accept an identity-less connection with zero scopes, the subscription that must precede chat.send, every connection hearing every Run's frames, and the caret on the calendar-versioned @openclaw/* dependencies and why it stays. The Runtime's doc comment said a limited Run's recorded Usage can sit below the cap it was stopped for; the record that sends the abort is the one that took the total past the limit, so the records already exceed it. What the stopped call costs is what is missing, and the sentence now says the recorded Usage understates what the Run cost. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Example 06 is the minimal HTTP application with createOpenClawRuntime in place of createPiRuntime: the daemon from its published image at one pinned version, paired once by an Ed25519 device key the Gateway loads from a mounted PEM, reached by service name over the private network with OPENCLAW_ALLOW_INSECURE_PRIVATE_WS set, configured from a read-only JSON file that resolves the shared token and the Anthropic key from the environment, with no channels, the control UI off and its state on a named volume so the approval survives restarts. The README states what differs from running pi, how to generate a key, and the one approve command. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A socket the daemon closes before hello-ok without a connect error reaches only the client's onClose hook, where the Runtime rejected the opening promise and left the client running, so it reconnected every second for as long as the process lived after the Run had failed. The hook now stops the client before it returns, as the onConnectError branch does, which is before the client schedules the reconnection. The fake daemon can close a socket before its challenge, and a test holds the Run to one failed connection and no second one. The close in run's finally is the client's stopAndWait, which rejects after its own timeout if the socket is still open; unguarded, that rejection would have replaced the outcome and its Usage. It is caught and logged, and the outcome stands. No test: the client arms a 250 ms force-terminate that resolves the stop before its timeout can fire, so the rejection is not reachable through the fake daemon. The connection target loses the agentId it never read, KeyObject is imported once as the value it is, the container test's daemon configuration drops the retired default agent marker to match the example's, the fake daemon's scope map loses the sessions.list entry nothing sends, and the session name cap stops being exported since the tests state the literal. The Session grammar is stated once, in the createOpenClawRuntime doc comment the reference site renders, and session-names.ts points at it. The helpers every OpenClaw test file copied live in src/test-support/openclaw-runtime.ts, waitFor gives way to waitUntil, and the comments that pointed at numbered findings in a file outside the tree state the fact instead. The example's README says the daemon's container and session where it said agent container and conversation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Built on #34 (
feat/parallelization); merge that PR first. Most of this PR is tests, test helpers, an example, and documentation for the new runtime.What this does
Adds
createOpenClawRuntime, exported from@shutter-network/concorde/openclaw, as an alternative to the pi runtime. It connects Concorde to an OpenClaw daemon. The gateway, components, and signal handlers work with either runtime.To switch the application to OpenClaw, replace the runtime constructor:
One run, end to end
sequenceDiagram participant W as Signal worker participant R as OpenClaw runtime participant D as OpenClaw daemon W->>R: run(prompt) R->>D: Connect with token and signed device challenge (operator.read + operator.write) R->>D: sessions.messages.subscribe { key: session } R->>D: chat.send { sessionKey: session, message, deliver: false } D-->>R: session.message with usage and __openclaw.runId, once per model call D-->>R: chat with state final, error, or aborted and runId R->>D: Close connection R-->>W: { ok, usage[], limited? }The daemon broadcasts events for multiple runs over each connection. The runtime filters run events by
runIdand usage records by__openclaw.runId. It subscribes before sending the prompt so it does not miss usage records.Every send uses
deliver: false, which disables replies through OpenClaw's own channels. The agent must use Concorde's messenger to send messages to users.How it differs from pi
sessionsDirand follow pi's naming rules.agent:<agentId>:<name>. They must be nonblank, lowercase, at most 512 characters, and contain no colons or surrounding whitespace.mainandglobalare reserved. These checks prevent OpenClaw's name normalization from merging distinct sessions.chat.abort. OpenClaw does not report usage for the interrupted call, so the recorded total can understate the run's cost.Each runtime now validates its own session names. Both accept
run_<uuid>, the format the worker uses when it creates a session. Tests cover that shared requirement.What is in the tree
Adds
@openclaw/gateway-clientand@openclaw/gateway-protocolat^2026.8.1. Imports are restricted tosrc/openclaw/by the Biome configuration and a test. Connections require a matching protocol version, so an incompatible daemon produces a connection error. The container test uses a pinned daemon image.Verification
Validation reported for this branch:
npm run check: 634 tests pass.npm run check:packageandnpm run check:docspass.npm run test:containertests a real OpenClaw daemon: an unapproved device gets pairing instructions, approval through the daemon's CLI allows subsequent runs, two prompts continue the same session and return usage, and exceeding a usage limit returnslimited: truewith one usage record.06_openclawexample was run under Compose using a locally packed package. Device pairing, pairing approval surviving a restart, and a run reaching the model provider were checked. Sending an agent reply through the messenger was not verified because no model key was available.Notes for the reviewer
/openclawbefore it can run without local package overrides.totalTokensis calculated asinput + output + cacheRead + cacheWrite, as required byUsageReport. This also matches the observed daemon records.