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
43 changes: 28 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ Docker or the network:
every `exports` subpath resolves there for Node *and* the type checker. It is also the only thing
anywhere that holds the fourteen table names apart, and the only proof that the root still
resolves to nothing.
- **`npm run test:container`** is the single end-to-end test: a real container running real `pi`
against the real Agent server, with a scripted model on localhost, so it needs no model
credentials. About ten seconds. `CONCORDE_CONTAINER_TESTS` opts in.
- **`npm run test:container`** runs both agent implementations in containers against a scripted
model, without model credentials. `src/pi/agent-instance.test.ts` covers pi;
`src/openclaw/agent-instance.test.ts` covers pairing, session continuity, usage, and usage limits
against the image pinned in `src/test-support/docker.ts`. Run these when changing a runtime or
upgrading an agent implementation: the scripted daemons cannot detect protocol drift.
`CONCORDE_CONTAINER_TESTS` opts in.
- **`npm run check:docs`** regenerates `site/reference`, asserts every page still holds a
preformatted block with a link inside it, and builds the site.

Expand Down Expand Up @@ -124,9 +127,9 @@ Four ways that fails quietly, all of them a deployment's to get right:
Declared by the Messenger, the Nostr Channel (two), Password Auth (two) and Nostr Auth, in the
`references(() => users.id)` calls. `src/schemas.test.ts` pushes every part's schema together,
which is what keeps the assembled set honest.
- **Three libraries are confined to the component that owns each, in one Biome `overrides`
entry.** `pg` is the Db's, `jose` is Signatures', and `nostr-tools`/`@nostrify/nostrify` belong to
the two parts that speak Nostr. **Add a confinement to that entry and never as a second one**:
- **Keep library import restrictions in one Biome `overrides` entry.** `pg` belongs to the db,
`jose` to signatures, `nostr-tools`/`@nostrify/nostrify` to the Nostr channel and Nostr auth,
and `@openclaw/*` to the OpenClaw runtime. **Extend that entry instead of adding another**:
Biome applies the *last* matching entry per rule and **replaces** its configuration rather than
merging, so a second entry naming the same rule silently disables the first. A `//` comment
anywhere in `biome.json` disables the overrides too, with no parse error. Neither says anything on
Expand Down Expand Up @@ -174,20 +177,30 @@ Four ways that fails quietly, all of them a deployment's to get right:
the previous connection happened to leave open. `success: true` carrying `cancelled: true`, which
is an extension of the Operator's refusing the switch, is the same failure by a documented route
and is refused two lines above it.
- **`sessionNames` in `src/pi/runtime.ts` is a transcription of `pi`'s own `assertValidSessionId`,
and nothing anywhere checks that it still matches.** The framework never had to carry one before:
`pi` was handed `--session-id` and refused a bad one itself, with its own message. A Session is
addressed **by path** over RPC now and `pi` opens any path it is handed, so the grammar has to
live on this side and a copy goes stale in silence. It is also the whole of the traversal
argument, there being no `/` in it and both ends required to be alphanumeric, so nothing resolves
a path and compares it against a prefix. Widening that character class is therefore a Signal
Handler's Session name climbing out of `sessionsDir` and not a cosmetic edit; re-read
`core/session-manager` against it whenever the pinned `pi` version moves.
- **Keep `sessionNames` in `src/pi/runtime.ts` aligned with pi's `assertValidSessionId`.**
pi opens any session path it receives, so this check also prevents a session name from escaping
`sessionsDir`. Review it against `core/session-manager` when upgrading pi. Other runtimes define
their own restrictions; all must accept `run_<uuid>` for a fresh session.
- **Review OpenClaw's session-key normalization when upgrading `openClawImage`.**
`refusedSessionName` prevents distinct session names from resolving to one session. The scripted
daemon does not reproduce normalization, so unit tests cannot detect changes to those rules.
- **Keep `deliver: false` on `chat.send`.** Enabling OpenClaw's own delivery could send replies
outside the messenger and leave them out of the message log.
- **Pass an explicit `deviceIdentity` and supply the crypto `hostDeps` in
`src/openclaw/connection.ts`.** The client's default identity loader returns no identity. A
token-only connection can complete the handshake with zero scopes, then fail every run method
with `MISSING_SCOPE` without opening a pairing request. The crypto hook defaults throw.
- **Subscribe to session messages before `chat.send` and filter frames by `runId`.**
Late subscriptions miss usage records; broadcasts include other runs. The runtime tests cover
request ordering and unrelated frames; the container test checks the real subscription behavior.
- **`src/chat-tui/` is the one shipped directory that is not a subpath.** It is a `bin`, so
the export map is untouched. **It has zero dependencies and must keep them**: a dependency added
here lands in every consumer's install, and the answer to needing one is a second package.
- **`dependencies` carry caret ranges; `devDependencies` are pinned exactly.** The lockfile is
committed, so exact pins in `dependencies` buy nothing and cost a consumer duplicate installs.
OpenClaw's calendar-versioned caret ranges allow updates within the same year. Protocol-version
negotiation rejects incompatible protocol versions, but cannot detect behavior changes within
one version. Check client updates against the pinned `openClawImage` with the container test.
- **Anything shipped is proven by `npm run check:package`, not by reading `files`.** Every public
export must appear in the annotated `main.ts` that check writes, and anything reaching for a new
runtime dependency must be *imported and called* in its runtime step — that is what proves the
Expand Down
8 changes: 4 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The HTTP server exposed outside the Gateway. Named for its exposure rather than
_Avoid_: user server, external API, frontend

**Agent server**:
The HTTP server only the Agent Implementation reaches, carrying the Signal Worker's Signal and Run routes plus whatever Producers expose to the agent. A mediation point, not a security boundary against the agent. **It is one of two unauthenticated privileged addresses on the agent network, and they point opposite ways.** This one is inbound: the agent reaches it and no credential is asked for. The **Agent Instance**'s RPC is outbound: the Gateway reaches that and no credential is asked for there either, `pi`'s RPC having no authentication of any kind and a command set that includes `bash`. Which is worth saying plainly rather than leaving to be discovered, and is still not a new trust: the network is the boundary for both, it is the same boundary the Agent server has always rested on, and what changed is only that it now carries traffic in both directions.
The HTTP server the agent implementation uses to reach the signal worker's signal and run routes and any routes exposed by producers. The agent server accepts requests without credentials and must remain on the private agent network. The gateway also reaches the agent instance on that network. A pi RPC listener has no authentication and can execute shell commands. The OpenClaw daemon requires a shared token and a signed device challenge, with device approval and scopes controlled by the operator. Both addresses are privileged; the network remains their security boundary.
_Avoid_: internal API, private server, control plane

**Db**:
Expand Down Expand Up @@ -105,7 +105,7 @@ What the Signal Worker hands a Prompt to and gets an outcome back from, and the
_Avoid_: runtime adapter, driver, plugin, connector, backend

**Agent Instance**:
A running Agent Implementation the Operator hosts, which the Gateway reaches over the RPC channel it accepts on. The Operator owns its whole environment (its image, its files, its model credential, its flags) and the framework starts it, stops it and configures it not at all. One connection is opened per Run and closed at the end of it; whether one process serves every connection or the listener starts one per connection is the Operator's arrangement, and nothing in the framework can tell the difference or depends on the answer. That last clause is the entry's one piece of work: with a listener that forks, "instance" names a thing that is a socket most of the time and a process only while a Run is happening, and the word is kept anyway because what a Runtime addresses is one place where the agent can be reached. **Agent Endpoint** was the considered alternative and collides with the **Agent server**, which is an address pointing the other way; **Agent Host** was refused because the **Operator** entry bans "host" outright. This term replaced four that named a mechanism the framework no longer has, the declaration of a container the Gateway started and of what that container could see on disk. None of it is gone as a practice: it moved into the Operator's compose file, which is where a container has always been better expressed, and the framework has no name for any of it now.
A running agent implementation reached by the gateway at an address provided by the operator. The operator configures and runs it, including its image, files, model credentials, and tools. Each run opens a connection and closes it when finished. An agent instance may serve every connection in one process, as the OpenClaw daemon does, or start a process per connection, as a pi RPC listener can. The runtime does not depend on that choice. The term describes where the agent can be reached, even when no agent process exists between runs.
_Avoid_: agent container, agent endpoint, agent host, agent service, sidecar

**OpenClaw daemon**:
Expand Down Expand Up @@ -143,7 +143,7 @@ It receives two values:
_Avoid_: post handler, teardown, finalizer, callback

**Session**:
The Agent Implementation's unit of conversational continuity. A Signal Handler routes each Prompt to a fresh or a named Session. Organises context; does not isolate it. A Handler asking for a fresh one is answered by the Signal Worker, which names it after the Run it belongs to before any Runtime sees it. So every Run records the Session it actually used, and no Runtime invents a naming convention of its own.
The agent implementation's unit of conversational continuity. A signal handler selects a named session or requests a fresh one. Sessions preserve conversational history but do not restrict access. The signal worker names a fresh session `run_<uuid>` using the run's id, so every run records the session it used. Each runtime defines which session names it can address and returns a failed outcome for an unsupported name. Every runtime must accept the names the signal worker generates.
_Avoid_: chat, conversation, thread, context (as a name for a Session; a Prompt's Context, below, is a different thing)

**Run**:
Expand Down Expand Up @@ -181,7 +181,7 @@ The token limit for one Run. A Signal Handler sets it on the Prompt as `usageLim

The Runtime adds up tokens as each model call finishes. If the total exceeds the limit, it stops the agent. The Run fails with `limited: true`, and its outcome still carries the usage of every call, including calls that finished after the stop. The Session keeps the history saved so far.

Usage is only known after a model call finishes. This means the Run can go over the limit by one call.
Usage is known only after a model call finishes, so a run can exceed its limit while another call is in progress. pi reports partial usage for an interrupted call. The OpenClaw daemon reports none for that call, so a limited run's saved usage can understate the tokens it used.

Every token counts equally toward this limit, including tokens read from the provider's cache. Each model call sends the agent's full context again. The provider reports most of that repeated context as cache reads. Cache reads therefore usually make up most of the token count, even though they cost much less.

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ the gateway manages their lifecycle.
![Gateway components and their connections to the agent instance, a user client, and a Nostr relay.](./site/public/architecture.svg)

The two channels in the diagram are alternatives: a messenger currently accepts one channel.
You run the agent instance separately. The included pi runtime connects to it over RPC; it does
not configure the model, credentials, tools, or container.
You run the agent instance separately. Use the pi runtime for a pi RPC listener or the
OpenClaw runtime for the authenticated WebSocket connection to the OpenClaw daemon. The operator
configures the model, credentials, tools, and container.

## Try it

Expand Down Expand Up @@ -73,12 +74,14 @@ from the registry rather than this checkout.
| [Nostr](./examples/03_nostr/) | Encrypted direct messages through a local relay |
| [Agent isolation](./examples/04_agent_isolation/) | Restricted agent networking, a model proxy that holds the credential, and per-run rules enforced by run tag |
| [Usage limits](./examples/05_usage_limits/) | Two users, each with a daily token limit |
| [OpenClaw](./examples/06_openclaw/) | The minimal application with the OpenClaw daemon as the agent implementation, paired once by device key |

## Deployment boundaries

All parties must trust the operator. The agent server and agent RPC listener are privileged,
unauthenticated endpoints; keep them on a private network. Configure agent permissions, TLS,
rate limiting, secret management, and backups for your application.
All parties must trust the operator. Keep the agent server and agent instance on a private
network. The agent server and a pi RPC listener accept connections without authentication. The
OpenClaw daemon requires authentication but also provides privileged access to the agent. Configure
agent permissions, TLS, rate limiting, secret management, and backups for your application.

Concorde does not prevent prompt injection or enforce confidentiality between users. Use `quote`
to mark user message boundaries in prompts and reduce prompt injection risk. A valid
Expand Down
5 changes: 5 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"!src/signatures/**",
"!src/nostr-channel/**",
"!src/nostr-auth/**",
"!src/openclaw/**",
"!src/test-support/**"
],
"linter": {
Expand All @@ -67,6 +68,10 @@
{
"group": ["nostr-tools", "nostr-tools/**", "@nostrify/**"],
"message": "Nostr belongs to the two parts that speak it, the Nostr Channel and Nostr Auth, the way pg belongs to the Db: nothing from either library crosses this framework's API boundary, so they are ordinary dependencies rather than peers, and a third part reaching for one would make them something a consumer has to install. src/test-support/** is exempt too, because a real client is the only thing that proves the fake Relay is a Relay and a real signer the only thing that proves a NIP-98 forgery is refused."
},
{
"group": ["@openclaw/**"],
"message": "Import OpenClaw packages only in src/openclaw/** or src/test-support/**. The OpenClaw runtime owns the protocol; other components must remain independent of the agent implementation."
}
]
}
Expand Down
2 changes: 2 additions & 0 deletions examples/06_openclaw/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
5 changes: 5 additions & 0 deletions examples/06_openclaw/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ANTHROPIC_API_KEY=

USER_PASSWORD=correct horse battery staple

OPENCLAW_GATEWAY_TOKEN=insecure-example-only-daemon-token
27 changes: 27 additions & 0 deletions examples/06_openclaw/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Shared agent instructions

You help one user through a gateway. Keep answers brief.

## Gateway API

Use the `exec` tool and `curl` to reach the gateway at `http://gateway:7411`. No credential is
required, but every prompt names a run tag, and every request you make to the gateway must carry
it as the `concorde-run-tag` header. Copy the tag from the prompt that woke you, never from an
earlier one. Read the live API description before calling it:

```sh
curl -s -H "concorde-run-tag: <the tag from your prompt>" http://gateway:7411/openapi.json
```

Use that document for request fields and error responses. If a call is refused, check the
response and correct the request before retrying. Do not repeatedly send the same invalid call.

## Reply to the user

Send replies with `POST /messages`, the `concorde-run-tag` header and body
`{"userId":"...","text":"..."}`. Copy `userId` from the triggering signal; never invent one.
Your final model response and workspace files are not delivered to the user.

Read prior messages with `GET /messages?user=<id>`. The message log contains both directions;
use its cursors when paging. Write short, plain sentences without headings, tables, or code
blocks. The terminal client polls once a second, so delivery may follow your run.
10 changes: 10 additions & 0 deletions examples/06_openclaw/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM node:24-alpine

WORKDIR /app

COPY package.json ./
RUN npm install --no-audit --no-fund

COPY main.ts drizzle.config.ts schema.ts ./

CMD ["node", "main.ts"]
Loading