You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: third-person voice across specs, runbooks, analysis
Strip first-person team-channel language from internal-history documents
so they read as technical reference material. README and formal
artifacts (whitepaper, IETF drafts, research papers) untouched.
Copy file name to clipboardExpand all lines: ANALYSIS-per-prompt-injection.md
+11-11Lines changed: 11 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
**Goal:** every time an openclaw LLM run starts a new turn, the pilot directive is in front of the model. No exceptions, no "fell out of context after compaction", no "only loaded if the user opens HEARTBEAT.md".
4
4
5
-
**Status:** the daemon currently writes content to `HEARTBEAT.md` + `CLAW.md`, but neither is on the per-prompt path. We have to move to a real per-prompt surface.
5
+
**Status:** the daemon currently writes content to `HEARTBEAT.md` + `CLAW.md`, but neither is on the per-prompt path. The daemon must move to a real per-prompt surface.
Critical detail (verified at `attempt.ts:1203-1216`): **all four returned fields are joined across multiple plugins via `joinPresentTextSegments` — plugins compose, they don't overwrite.** That means if the user has their own plugins that contribute to the system context, ours can add to it cleanly.
28
+
Critical detail (verified at `attempt.ts:1203-1216`): **all four returned fields are joined across multiple plugins via `joinPresentTextSegments` — plugins compose, they don't overwrite.** That means if the user has their own plugins that contribute to the system context, the pilot plugin can add to it cleanly.
29
29
30
30
The hook runs **every single turn**. There is no "first message only" gating at this layer. Compaction-survival, fresh-resume, mid-session — irrelevant. If the plugin is loaded, the hook fires.
- Composes with user plugins (`joinPresentTextSegments` does the right thing)
51
51
- Doesn't touch the user's `openclaw.json` — clean operational footprint
52
52
- Failure-isolated: `runBeforePromptBuild` wraps in `try/catch` and warns if a hook throws, so a bad pilot version can never break the user's openclaw
53
-
- Already part of openclaw's first-class plugin API; survives openclaw upgrades unless they break the hook contract (which would break many plugins, not just ours)
53
+
- Already part of openclaw's first-class plugin API; survives openclaw upgrades unless they break the hook contract (which would break many plugins, not just the pilot one)
54
54
55
55
**What needs to ship:**
56
56
- A small openclaw plugin package — `@pilotprotocol/openclaw-plugin` on npm — registering the hook
- Conflicts with user-set `systemPrompt`; needs the `_pilotManaged` sentinel + prepend-with-divider merge logic from the previous spec
77
77
- Edits a config file the user owns — risk of user reverting or the merge logic having bugs
78
-
- Only fires the `systemPrompt` field, not the richer `prependSystemContext` (which lets us coexist with user plugins)
78
+
- Only fires the `systemPrompt` field, not the richer `prependSystemContext` (which permits coexistence with user plugins)
79
79
80
-
**When to use:** as the path for users who haven't installed our plugin yet, or whose openclaw version is too old to support `before_prompt_build` hooks.
80
+
**When to use:** as the path for users who haven't installed the pilot plugin yet, or whose openclaw version is too old to support `before_prompt_build` hooks.
If a user has multiple openclaw agents and we want per-agent control, the schema (`src/config/zod-schema.providers-core.ts`) supports `systemPrompt` at the agent level. Same mechanism, narrower scope. Useful for a future "pilot-only" agent.
84
+
For per-agent control when a user has multiple openclaw agents, the schema (`src/config/zod-schema.providers-core.ts`) supports `systemPrompt` at the agent level. Same mechanism, narrower scope. Useful for a future "pilot-only" agent.
85
85
86
86
## Why HEARTBEAT.md doesn't cut it
87
87
@@ -99,7 +99,7 @@ The current state — `HEARTBEAT.md` carries the full directive — only reaches
99
99
|**hermes**| Not installed locally; manifest writes to `~/.hermes/SOUL.md` (loaded as `selfHeartbeat`-equivalent) | n/a — would need user install | Hook surface unverified — if/when a user installs hermes, investigate before committing |
100
100
|**openhands**| Not installed locally; manifest uses `selfHeartbeat: true` flag — implies openhands has its own pulling-in mechanism via `microagents/`| n/a — needs install | Pull-based; per-prompt likely handled by openhands itself once microagent exists |
101
101
102
-
The pattern across the four "Claude-like" tools (Claude Code / openclaw / picoclaw / hermes) is convergent: each exposes a *pre-LLM-call hook* that takes content and prepends it to the system or user context. The pilot fix is the same shape every time — register a hook, return our text — only the wire format differs per tool.
102
+
The pattern across the four "Claude-like" tools (Claude Code / openclaw / picoclaw / hermes) is convergent: each exposes a *pre-LLM-call hook* that takes content and prepends it to the system or user context. The pilot fix is the same shape every time — register a hook, return the pilot text — only the wire format differs per tool.
103
103
104
104
## Why "a while ago this was the case" — the regression
105
105
@@ -109,7 +109,7 @@ The user has mentioned this was working previously. Likely cause: an earlier ope
109
109
110
110
2.**Openclaw's `systemPromptWhen` default may have changed** at some point from `"always"` to `"first"`. If users had pilot text in `systemPrompt` and openclaw used to send it every turn, the directive used to land. Today's default = `"first"` means it only lands on turn 1.
111
111
112
-
Either way, the daemon's current behavior (write to HEARTBEAT.md + leave systemPromptWhen alone) is *insufficient* on current openclaw. We need to actively manage one of the per-prompt surfaces.
112
+
Either way, the daemon's current behavior (write to HEARTBEAT.md + leave systemPromptWhen alone) is *insufficient* on current openclaw. The daemon must actively manage one of the per-prompt surfaces.
113
113
114
114
## Recommended path forward (openclaw-first)
115
115
@@ -138,12 +138,12 @@ Phase 1 is the unblock. Phase 2 is the durable answer. Phase 3 generalizes.
138
138
139
139
## Open questions to resolve before implementation
140
140
141
-
1.**`heartbeats/openclaw.md` reuse**: should we trim the existing 104-line heartbeat for system-prompt-on-every-turn, or build a separate, tighter template? My recommendation: separate `heartbeats/openclaw-system-prompt.md` capped at ~600 tokens, since this rides on every turn's context budget.
141
+
1.**`heartbeats/openclaw.md` reuse**: should the existing 104-line heartbeat be trimmed for system-prompt-on-every-turn, or should a separate, tighter template be built? Recommendation: separate `heartbeats/openclaw-system-prompt.md` capped at ~600 tokens, since this rides on every turn's context budget.
142
142
2.**Daemon→openclaw bridge for plugin install**: does the daemon need access to `openclaw plugins install ...`, or does it write directly to `~/.openclaw/plugins/installs.json`? The latter is fragile (file format owned by openclaw); the former requires shelling out. Likely answer: shell out to `openclaw plugins install @pilotprotocol/openclaw-plugin` and treat openclaw-binary-not-on-PATH as a soft-fail.
143
-
3.**Versioning**: when openclaw bumps its plugin contract (the `hostContractVersion`we saw at `2026.5.7` in `installs.json`), do we need to re-publish our plugin? Yes — but the plugin can declare a compat range.
143
+
3.**Versioning**: when openclaw bumps its plugin contract (the `hostContractVersion`observed at `2026.5.7` in `installs.json`), does the pilot plugin need to be re-published? Yes — but the plugin can declare a compat range.
144
144
4.**Disable for power users**: same opt-out machinery as the config mutation spec — env var + sentinel + CLI command.
Copy file name to clipboardExpand all lines: INVESTIGATION-cloudflare-workers.md
+6-6Lines changed: 6 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -64,7 +64,7 @@ Run a regular daemon on conventional infra (VM, k8s pod, Fly.io, etc.) with full
64
64
65
65
**Pros:**
66
66
- Worker becomes a thin client; nothing about its identity is special-cased on the wire (it inherits the bridge's identity, or has its own which the bridge proxies for).
67
-
-We can ship in days, not months — the bridge is just an HTTP wrapper around `driver.Driver`.
67
+
-Shippable in days, not months — the bridge is just an HTTP wrapper around `driver.Driver`.
68
68
- TLS termination at the bridge; auth via a per-Worker bearer token.
69
69
70
70
**Cons:**
@@ -92,7 +92,7 @@ Build a pure-TypeScript implementation of the Pilot wire protocol (handshake/PIL
92
92
- Long-term answer: the relay can later be replaced by direct UDP if Workers ever gains it.
93
93
94
94
**Cons:**
95
-
- Substantial new TS implementation — the Go protocol code is ~10k LOC, the subset we'd port is ~2–3k LOC (envelope + handshake + replay-window + key-exchange + ECDH/AEAD via WebCrypto).
95
+
- Substantial new TS implementation — the Go protocol code is ~10k LOC, the subset to port is ~2–3k LOC (envelope + handshake + replay-window + key-exchange + ECDH/AEAD via WebCrypto).
96
96
- Have to keep the TS implementation in sync with future Go changes (the existing FFI approach has the Go code as single source of truth).
97
97
- WebSocket → UDP mapping has its own edge cases (frame ordering, MTU, reconnects, NAT-keepalive equivalent).
98
98
- Worker CPU budget (typical 50–100 ms per request, or up to 5 min on "unbound") needs careful pacing for handshakes + retransmits.
@@ -133,9 +133,9 @@ The TypeScript side owns sockets, timers, retx loops, and feeds bytes into the W
133
133
|---|---|---|
134
134
| Worker bundle, gzipped | 3 MB free / 10 MB paid | A standard Go WASM is ~10 MB raw, ~3 MB gzipped — **right at the free-tier ceiling, fits paid**|
| WASI support | "experimental, only some syscalls" | irrelevant — we'd use the host-import bridge, not WASI |
136
+
| WASI support | "experimental, only some syscalls" | irrelevant — the host-import bridge is used, not WASI |
137
137
| Go→WASM target |`GOOS=js GOARCH=wasm` (browser-shim) or `GOOS=wasip1 GOARCH=wasm`| use `wasm_exec.js`-style shim with custom host imports |
138
-
| TinyGo size |~200–500 KB for our subset | better fit, *but* TinyGo's incomplete support for goroutines / channels / reflect would force us to refactor away those constructs in the pilot packages we compile|
138
+
| TinyGo size |~200–500 KB for the relevant subset | better fit, *but* TinyGo's incomplete support for goroutines / channels / reflect would force a refactor away from those constructs in the compiled pilot packages |
@@ -160,8 +160,8 @@ The TypeScript side owns sockets, timers, retx loops, and feeds bytes into the W
160
160
**Cons:**
161
161
- Two languages for one library — debugging spans both.
162
162
- Goroutine→TS event-loop bridging is non-trivial. The WASM module must be designed so all its "callbacks" surface as deterministic return values rather than spawning goroutines.
163
-
- Standard Go's WASM runtime is ~3 MB gzipped — right at the free-tier ceiling. TinyGo would solve that but cuts off goroutines, which the asymmetric-recovery code relies on (background retx). We'd need to refactor those into "step functions" the TS driver calls each tick.
164
-
- Need a custom `wasm_exec.js` shim that imports Workers' `connect()` / `crypto.subtle` / `setTimeout` into the wasm host. Cloudflare's WASI is experimental and lacks sockets, so we can't lean on it.
163
+
- Standard Go's WASM runtime is ~3 MB gzipped — right at the free-tier ceiling. TinyGo would solve that but cuts off goroutines, which the asymmetric-recovery code relies on (background retx). Those would need to be refactored into "step functions" the TS driver calls each tick.
164
+
- Need a custom `wasm_exec.js` shim that imports Workers' `connect()` / `crypto.subtle` / `setTimeout` into the wasm host. Cloudflare's WASI is experimental and lacks sockets, so it cannot be relied on.
165
165
166
166
**Effort estimate:**~4–6 weeks. Faster than Option B (no protocol re-implementation), slower than Option A (still need the wasm host shim + a redesigned step-function-friendly protocol surface in the Go code).
Copy file name to clipboardExpand all lines: RUNBOOK-pilot-ca.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@
8
8
9
9
**The Pilot CA is NOT yet in production use.** The compat-mode WSS endpoint at `beacon.pilotprotocol.network` currently uses a **Let's Encrypt** certificate, terminated by nginx on the rendezvous host. The daemon's `-tls-trust` default is `system`, which verifies against the OS trust store.
10
10
11
-
This runbook describes the future-hardening path: when we mint the production Pilot root, embed it in client binaries, and flip `-tls-trust=pinned`as the default, attacks that compromise a public CA can no longer MITM compat daemons. Until that release ships, the Pilot CA tooling below is rehearsal-only.
11
+
This runbook describes the future-hardening path: once the production Pilot root is minted, embedded in client binaries, and `-tls-trust=pinned`becomes the default, attacks that compromise a public CA can no longer MITM compat daemons. Until that release ships, the Pilot CA tooling below is rehearsal-only.
12
12
13
13
## The trust model in one paragraph
14
14
@@ -94,7 +94,7 @@ Leaf certs expire after 90 days. Re-issue at day 75 so there's a 15-day overlap
94
94
95
95
Daemons don't notice — TLS sessions in flight stay valid; new connections use the new cert.
96
96
97
-
Automation candidate: drive this from a periodic cron on the custody host, since the root key has to be online briefly. Out of scope for v1 — manual ops is fine while we have one or two beacons.
97
+
Automation candidate: drive this from a periodic cron on the custody host, since the root key has to be online briefly. Out of scope for v1 — manual ops is fine while only one or two beacons exist.
98
98
99
99
## Phase 5 — Root rotation (every 5-10 years, or after compromise)
100
100
@@ -127,5 +127,5 @@ For a compromise-driven rotation, compress the window aggressively — flip to t
127
127
## What the tool deliberately does NOT do
128
128
129
129
- No automatic OCSP / CRL stapling. Compat mode is a small, controlled PKI — short-lived leaves + fast embed rotation are simpler than CRL distribution.
130
-
- No ACME. ACME requires DNS or HTTP control of the validated domain, which exposes a public DNS/HTTP attack surface we don't otherwise need. Manual `pilot-ca issue-beacon` is the right granularity until we have hundreds of beacons.
130
+
- No ACME. ACME requires DNS or HTTP control of the validated domain, which exposes a public DNS/HTTP attack surface the deployment otherwise does not need. Manual `pilot-ca issue-beacon` is the right granularity until the fleet reaches hundreds of beacons.
131
131
- No HSM integration in the binary. The operator's custody is whatever they choose (Yubikey via OpenSSL engine, GPG smartcard, air-gapped Linux). `pilot-ca init-root` writes a software key; the operator is responsible for moving it.
Copy file name to clipboardExpand all lines: SPEC-compat-mode.md
+6-6Lines changed: 6 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,7 +58,7 @@ The beacon is the universal hub. UDP peers don't know whether a remote is on UDP
58
58
59
59
### Pilot root CA
60
60
61
-
-We mint our own root CA keypair (offline, e.g. on a Yubikey).
61
+
-A dedicated root CA keypair is minted offline (e.g. on a Yubikey).
62
62
- The root CA's PEM-encoded certificate is **embedded in `cmd/daemon` via `//go:embed`** so every daemon binary ships with the trust anchor pre-pinned.
63
63
- The CA signs leaf certs for each beacon hostname (`beacon-us.pilotprotocol.network`, `beacon-eu.…`, etc.).
64
64
- Leaf certs rotate via standard `tls.Config.GetCertificate`. Root rotation is a multi-release event handled by shipping the new root in a daemon update alongside the old one (overlap window).
@@ -108,7 +108,7 @@ The beacon is the universal hub. UDP peers don't know whether a remote is on UDP
108
108
109
109
### Transport interface
110
110
111
-
Today `pkg/daemon/udpio.Socket` owns the UDP FD and exposes `Send(frame []byte, dst *net.UDPAddr) error` and `Recv() (frame []byte, src *net.UDPAddr, err error)`. We extract that contract into a `daemonio.Transport` interface:
111
+
Today `pkg/daemon/udpio.Socket` owns the UDP FD and exposes `Send(frame []byte, dst *net.UDPAddr) error` and `Recv() (frame []byte, src *net.UDPAddr, err error)`. That contract is extracted into a `daemonio.Transport` interface:
112
112
113
113
```go
114
114
typeTransportinterface {
@@ -232,7 +232,7 @@ On the daemon:
232
232
233
233
Public WSS endpoint can be hit by anyone. Mitigations:
234
234
- Per-source-IP rate-limit on upgrade attempts (above).
235
-
- nginx in front lets us deploy fail2ban / rate-limit modules at the edge.
235
+
- nginx in front allows deploying fail2ban / rate-limit modules at the edge.
236
236
- Auth challenge requires the attacker to have a valid Pilot identity already registered — bots can't open holding-pattern WSS connections cheaply.
237
237
238
238
## Rollout — what shipped
@@ -295,11 +295,11 @@ All phases collapsed into a single deploy on 2026-05-18. Production at `pilot-re
295
295
4.**WS subprotocol negotiation.****Shipped:**`Sec-WebSocket-Protocol: pilot.v1` set on both sides.
296
296
5.**Multi-beacon WSS connections.****Deferred** — single conn per daemon for v1; multi-beacon redundancy is a future improvement.
297
297
298
-
## What we're NOT building
298
+
## Out of scope
299
299
300
-
- Centralized HTTPS REST gateway. That'd be a separate service (Phase 8+) that proxies HTTPS REST → Pilot WSS. Easy to add later once compat mode is solid; doesn't belong in v1 of compat mode itself.
300
+
- Centralized HTTPS REST gateway. That would be a separate service (Phase 8+) proxying HTTPS REST → Pilot WSS. Easy to add later once compat mode is solid; does not belong in v1 of compat mode itself.
301
301
- HTTP/3 / QUIC. The whole point of compat mode is to use TCP/443 which firewalls don't block. QUIC is UDP and would defeat the purpose.
302
-
- WebRTC. We considered it — the data-channel ICE machinery would give us NAT traversal for free — but WebRTC requires a signaling server *and* still uses UDP for the data plane. Doesn't help our problem.
302
+
- WebRTC. Considered — the data-channel ICE machinery would provide NAT traversal for free — but WebRTC requires a signaling server *and* still uses UDP for the data plane. Does not address the underlying constraint.
0 commit comments