@anusornneal/chat-relay 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,230 +1,311 @@
1
- # chat-relay
2
-
3
- Cloudflare Worker + Durable Objects relay that exposes one or more local Windows agents to ChatGPT through remote MCP.
4
-
5
- ```
6
- ChatGPT / MCP client
7
- |
8
- | Streamable HTTP MCP
9
- v
10
- Cloudflare Worker
11
- |
12
- +-- Registry Durable Object (users, agents, grants)
13
- |
14
- +-- Relay Durable Object per agent
15
- |
16
- | WebSocket
17
- v
18
- Local Agent
19
- - terminal
20
- - files
21
- - processes
22
- ```
23
-
24
- The relay is generic and has no Devflow-specific logic.
25
-
26
- ## Zero-checkout CLI
27
-
28
- End users do not need this repository. After the npm package is published, the normal command is:
29
-
30
- ```bash
31
- npx @anusornneal/chat-relay@latest remote
32
- ```
33
-
34
- First run starts a device-login flow, opens the browser, and shows a short code. The browser page signs in with a Chat Relay login/password; if that login does not exist yet, it creates the account. After approval the CLI receives scoped user-session and agent credentials, stores them outside the project, and connects the local agent.
35
-
36
- Subsequent runs reuse the local credentials:
37
-
38
- ```bash
39
- npx @anusornneal/chat-relay@latest remote
40
- ```
41
-
42
- Other commands:
43
-
44
- ```bash
45
- npx @anusornneal/chat-relay@latest login
46
- npx @anusornneal/chat-relay@latest status
47
- npx @anusornneal/chat-relay@latest logout
48
- ```
49
-
50
- On Windows the config defaults to `%LOCALAPPDATA%\chat-relay\config.json`. On macOS/Linux it uses `$XDG_CONFIG_HOME/chat-relay/config.json` or `~/.config/chat-relay/config.json`. Set `CHAT_RELAY_HOME` to override the location.
51
-
52
- Useful options:
53
-
54
- ```bash
55
- chat-relay remote --root "C:\Users\you\Projects"
56
- chat-relay login --name "Work PC"
57
- chat-relay login --no-open
58
- ```
59
-
60
- The published package locates its bundled local agent relative to the package itself, so commands work from any working directory. Existing repository users can still run `npm start`; legacy `.dev.vars` credentials are migrated once into the user-level config.
61
-
62
- ## Authentication and multi-user model
63
-
64
- - Browser/device login uses a short-lived device code. Raw user or admin tokens are not typed into the CLI.
65
- - New accounts use a unique login plus password. Passwords are stored only as salted PBKDF2-SHA256 hashes.
66
- - Device start/approval requests and failed password attempts are rate-limited.
67
- - CLI user sessions are opaque random tokens stored server-side only as hashes and expire after 90 days.
68
- - Re-authentication revokes the previous CLI session when possible.
69
- - Each local machine has an independent `agentId` and agent token; agent tokens are stored server-side only as hashes.
70
- - Agent ownership is enforced before an existing machine identity can be reused, preventing shared users from rotating another owner's agent credential.
71
- - Grants map users to agents with scopes: `read`, `write`, `terminal`, `process`, or `*`.
72
- - Logging out revokes the local user session and the owning machine credential.
73
- - `ADMIN_TOKEN` remains separate and protects administration routes.
74
- - The existing legacy owner token remains supported so current ChatGPT MCP URLs continue to work during migration.
75
-
76
- ChatGPT MCP supports OAuth 2.1 authorization-code login with PKCE S256. OAuth access tokens are audience-bound to `/mcp`; `offline_access` issues rotating refresh tokens. Dynamic client registration and protected-resource/authorization-server discovery are exposed for compatible MCP clients. The legacy `/mcp?key=<USER_TOKEN>` flow remains supported during migration. Browser/device login is for the zero-checkout local CLI and creates the same registry user/agent model used by OAuth.
77
-
78
- ## MCP tools
79
-
80
- Identity and agent routing:
81
- - `whoami`
82
- - `list_agents`
83
- - `ping_agent`
84
- - `get_config`
85
- - `get_recent_tool_calls`
86
-
87
- Filesystem:
88
- - `stat_path`
89
- - `list_directory`
90
- - `read_file`
91
- - `read_multiple_files`
92
- - `start_search`
93
- - `get_more_search_results`
94
- - `write_file`
95
- - `edit_block`
96
- - `create_directory`
97
- - `move_path`
98
- - `delete_path`
99
-
100
- Processes:
101
- - `list_processes`
102
- - `kill_process`
103
-
104
- Terminal:
105
- - `terminal_exec`
106
- - `terminal_start`
107
- - `terminal_start_shell`
108
- - `terminal_read`
109
- - `terminal_write`
110
- - `terminal_list`
111
- - `terminal_kill`
112
-
113
- Desktop-Commander-compatible aliases:
114
- - `start_process`
115
- - `read_process_output`
116
- - `interact_with_process`
117
- - `list_sessions`
118
- - `force_terminate`
119
-
120
- ## Administration
121
-
122
- Administration is intentionally separate from MCP. Set the Cloudflare `ADMIN_TOKEN` secret, keep the same value in the local ignored `.dev.vars` on the administrator machine, and use:
123
-
124
- ```
125
- npm run admin -- state
126
- npm run admin -- bootstrap default "Primary PC"
127
- npm run admin -- create-user "Alice"
128
- npm run admin -- create-agent "Work Laptop" work-laptop
129
- npm run admin -- grant <userId> <agentId> read,write,terminal,process
130
- npm run admin -- revoke <userId> <agentId>
131
- npm run admin -- enable-user <userId> false
132
- npm run admin -- enable-agent <agentId> false
133
- npm run admin -- rotate-user <userId>
134
- npm run admin -- rotate-agent <agentId>
135
- ```
136
-
137
- Create and rotate commands return the new raw token once. Store it on the corresponding client/agent; the registry retains only its hash.
138
-
139
- The initial `bootstrap` migrates the legacy `CALLER_TOKEN` and `AGENT_TOKEN` secrets into an owner user and the default agent so an existing installation can upgrade without changing its current ChatGPT URL or local agent token.
140
-
141
- ## Worker routes
142
-
143
- | Route | Authentication | Purpose |
144
- | --- | --- | --- |
145
- | `GET /health` | none | Worker health |
146
- | `GET /.well-known/oauth-protected-resource[/mcp]` | none | MCP protected-resource metadata |
147
- | `GET /.well-known/oauth-authorization-server` | none | OAuth authorization-server metadata |
148
- | `POST /register` | none | Dynamic registration for public PKCE clients |
149
- | `GET/POST /authorize` | login/password | OAuth authorization-code sign-in |
150
- | `POST /token` | public client + PKCE/refresh token | Access/refresh token exchange |
151
- | `POST /auth/device/start` | none | Start CLI device authorization |
152
- | `GET /device?user_code=<code>` | none | Browser sign-in/approval page |
153
- | `POST /auth/device/approve` | login/password + device code | Approve or create a user account |
154
- | `POST /auth/device/token` | device code | Exchange approved device code for session/agent credentials |
155
- | `GET /auth/me` | user Bearer token | Current user and permitted agents |
156
- | `POST /auth/session/revoke` | user Bearer token | Revoke only the current user session |
157
- | `POST /auth/logout` | user Bearer token | Revoke local session and owning agent credential |
158
- | `POST /mcp?key=<user token>` | user token | Streamable HTTP MCP |
159
- | `GET /agent?agentId=<id>` | agent Bearer token | Local agent WebSocket |
160
- | `GET /status?agentId=<id>` | user token | Agent online status |
161
- | `POST /relay?agentId=<id>` | user token | Direct JSON relay with scope checks |
162
- | `/admin/*` | admin Bearer token | User/agent/grant administration |
163
-
164
- ## Runtime limits and controls
165
-
166
- - Relay request/response message: 64 KiB.
167
- - One-shot terminal command: max 20 seconds.
168
- - Persistent terminal sessions: up to 8 running sessions per local agent.
169
- - Terminal output buffer: bounded in memory; completed sessions retained for 30 minutes.
170
- - Filesystem reads/writes are bounded and restricted to configured `ALLOWED_ROOTS`.
171
- - Search skips common heavy directories such as `.git`, `node_modules`, `.gradle`, `.idea`, and `.wrangler`.
172
- - The agent blocks a small set of high-risk system-management commands. This is defense in depth, not a security sandbox.
173
-
174
- ## Tests
175
-
176
- The legacy relay integration test remains available:
177
-
178
- ```
179
- npm run dev:test
180
- python test/integration.py
181
- ```
182
-
183
- The full MCP/multi-user smoke test expects a local Worker on port 8795 plus an attached local agent:
184
-
185
- ```
186
- node test/multiuser-smoke.mjs
187
- ```
188
-
189
- It verifies the MCP tool surface, agent routing, filesystem operations, search sessions, process listing, persistent terminals, scope enforcement, multi-agent selection, and token rotation.
190
-
191
- Device/browser login is covered by:
192
-
193
- ```bash
194
- TEST_RELAY_URL=http://127.0.0.1:8796 npm run test:device-auth
195
- ```
196
-
197
- OAuth discovery, DCR, PKCE, token exchange, refresh rotation, audience binding, MCP Bearer auth, and legacy-key compatibility are covered by:
198
-
199
- ```bash
200
- TEST_RELAY_URL=http://127.0.0.1:8796 npm run test:oauth
201
- ```
202
-
203
- After `npm pack`, the zero-checkout package smoke test installs and runs the tarball from a temporary directory:
204
-
205
- ```bash
206
- CHAT_RELAY_TARBALL=<path-to-tgz> TEST_RELAY_URL=http://127.0.0.1:8796 npm run test:zero-checkout
207
- ```
208
-
209
- The npm package is configured as `@anusornneal/chat-relay`. Publishing requires an authenticated npm account with access to that scope.
210
-
211
- ## CI and npm publishing
212
-
213
- GitHub Actions runs package verification on Node 20 and Node 24. A separate integration job starts a local Worker and verifies device auth, OAuth, and the zero-checkout tarball flow.
214
-
215
- Before publishing locally:
216
-
217
- ```bash
218
- npm run verify:publish
219
- ```
220
-
221
- This checks the npm file set, CLI entrypoint, Worker dry-run build, and production dependency audit. The package whitelist excludes .dev.vars, Worker source, tests, and repository-only files.
222
-
223
- The repository also includes a Publish npm workflow for version tags or manual dispatch. It supports npm trusted publishing through GitHub OIDC and can also use an NPM_TOKEN repository secret when configured.
224
-
225
- The first registry publish still requires npm authorization for the @anusornneal scope. After publishing, verify the exact public UX from a clean directory:
226
-
227
- ```bash
228
- npx @anusornneal/chat-relay@latest status
229
- npx @anusornneal/chat-relay@latest remote
230
- ```
1
+ # chat-relay
2
+
3
+ Cloudflare Worker + Durable Objects relay that exposes one or more local Windows agents to ChatGPT through remote MCP.
4
+
5
+ ```
6
+ ChatGPT / MCP client
7
+ |
8
+ | Streamable HTTP MCP
9
+ v
10
+ Cloudflare Worker
11
+ |
12
+ +-- Registry Durable Object (users, agents, grants)
13
+ |
14
+ +-- Relay Durable Object per agent
15
+ |
16
+ | WebSocket
17
+ v
18
+ Local Agent
19
+ - terminal
20
+ - files
21
+ - processes
22
+ ```
23
+
24
+ The relay is generic and has no Devflow-specific logic.
25
+
26
+ ## Quick start
27
+
28
+ The CLI is published on npm. You do **not** need to clone this repository or run `npm install`.
29
+
30
+ From any directory:
31
+
32
+ ```bash
33
+ npx @anusornneal/chat-relay@latest remote
34
+ ```
35
+
36
+ On the first run, Chat Relay opens a browser-based device login. Sign in, approve the computer, then return to the terminal. The CLI saves its credentials in your user profile and connects the local agent automatically.
37
+
38
+ Later, use the same command from anywhere:
39
+
40
+ ```bash
41
+ npx @anusornneal/chat-relay@latest remote
42
+ ```
43
+
44
+ A successful connection looks like:
45
+
46
+ ```text
47
+ Chat Relay Remote
48
+ -----------------
49
+ Agent: Primary PC (default)
50
+ Terminal: enabled
51
+ Desktop: disabled
52
+ Agent connected
53
+ ```
54
+
55
+ ## Zero-checkout CLI
56
+
57
+ Other commands:
58
+
59
+ ```bash
60
+ npx @anusornneal/chat-relay@latest login
61
+ npx @anusornneal/chat-relay@latest status
62
+ npx @anusornneal/chat-relay@latest logout
63
+ ```
64
+
65
+ On Windows the config defaults to `%LOCALAPPDATA%\chat-relay\config.json`. On macOS/Linux it uses `$XDG_CONFIG_HOME/chat-relay/config.json` or `~/.config/chat-relay/config.json`. Set `CHAT_RELAY_HOME` to override the location.
66
+
67
+ Useful options:
68
+
69
+ ```bash
70
+ chat-relay remote --root "C:\Users\you\Projects"
71
+ chat-relay login --name "Work PC"
72
+ chat-relay login --no-open
73
+ chat-relay remote --desktop
74
+ chat-relay remote --no-desktop
75
+ ```
76
+
77
+ The published package locates its bundled local agent relative to the package itself, so commands work from any working directory. Existing repository users can still run `npm start`; legacy `.dev.vars` credentials are migrated once into the user-level config.
78
+ ## Device lifecycle and recovery
79
+
80
+ - `chat-relay status` shows the signed-in account, readable device name/id, relay URL, access state, online/offline state, last-seen time, and granted scopes without printing secrets.
81
+ - One account can own multiple PCs. Each PC keeps its own agent id and credential, so devices remain independently visible and revocable.
82
+ - Admins can rename a device from the dashboard without changing its agent id or grants.
83
+ - Retiring a device invalidates its current machine credential and disconnects the live agent. That credential cannot reconnect until the device is authorized again.
84
+ - Recovery after retirement does not require copying tokens: run `chat-relay login --force`, complete browser authorization, then run `chat-relay remote`.
85
+ - A different account cannot reclaim another owner's active or retired agent id; a colliding login receives a separate device identity instead.
86
+
87
+ ## Dashboard administrator sessions
88
+
89
+ Browser dashboard access uses the same Chat Relay username/password accounts but requires an explicit global administrator entitlement. Successful dashboard login creates a short-lived opaque server-side session; the browser receives an HttpOnly, Secure, SameSite=Strict cookie plus a CSRF token for state-changing requests. `ADMIN_TOKEN` remains an operator/CLI recovery credential and must never be embedded in dashboard JavaScript, browser storage, or URLs.
90
+ ## Authentication and multi-user model
91
+
92
+ - Browser/device login uses a short-lived device code. Raw user or admin tokens are not typed into the CLI.
93
+ - New accounts use a unique login plus password. Passwords are stored only as salted PBKDF2-SHA256 hashes.
94
+ - Device start/approval requests and failed password attempts are rate-limited.
95
+ - CLI user sessions are opaque random tokens stored server-side only as hashes and expire after 90 days.
96
+ - Re-authentication revokes the previous CLI session when possible.
97
+ - Each local machine has an independent `agentId` and agent token; agent tokens are stored server-side only as hashes.
98
+ - Agent ownership is enforced before an existing machine identity can be reused, preventing shared users from rotating another owner's agent credential.
99
+ - Grants map users to agents with scopes: `read`, `write`, `terminal`, `process`, `desktop_read`, `desktop_control`, or `*`. Explicit legacy grants do not gain desktop access automatically.
100
+ - Logging out revokes the local user session and the owning machine credential.
101
+ - `ADMIN_TOKEN` remains separate and protects administration routes.
102
+ - The existing legacy owner token remains supported only as transitional compatibility while OAuth becomes the normal ChatGPT MCP path.
103
+
104
+ ChatGPT MCP should connect to the plain `/mcp` endpoint. Compatible clients discover OAuth 2.1 automatically, then use authorization-code login with PKCE S256. OAuth access tokens are audience-bound to `/mcp`; `offline_access` issues rotating refresh tokens. Dynamic client registration and protected-resource/authorization-server discovery are exposed for compatible MCP clients. The legacy `/mcp?key=<USER_TOKEN>` flow remains available only during migration. Browser/device login is for the zero-checkout local CLI and creates the same registry user/agent model used by OAuth.
105
+
106
+ ## MCP tools
107
+
108
+ Identity and agent routing:
109
+ - `whoami`
110
+ - `list_agents`
111
+ - `ping_agent`
112
+ - `get_config`
113
+ - `get_recent_tool_calls`
114
+
115
+ Filesystem:
116
+ - `stat_path`
117
+ - `list_directory`
118
+ - `read_file` - bounded by bytes; returns `truncated` and `nextOffset` when more lines remain
119
+ - `read_multiple_files` - bounded aggregate response; supports string paths or per-file `{ path, offset, length, maxBytes }` entries plus `maxTotalBytes`
120
+ - `start_search`
121
+ - `get_more_search_results`
122
+ - `write_file`
123
+ - `edit_block`
124
+ - `create_directory`
125
+ - `move_path`
126
+ - `delete_path`
127
+
128
+ Processes:
129
+ - `list_processes`
130
+ - `kill_process`
131
+
132
+ Terminal:
133
+ - `terminal_exec`
134
+ - `terminal_start`
135
+ - `terminal_start_shell`
136
+ - `terminal_read`
137
+ - `terminal_write`
138
+ - `terminal_list`
139
+ - `terminal_kill`
140
+
141
+ Desktop (Windows, opt-in):
142
+ - `screenshot` - returns a bounded MCP image content block plus coordinate metadata
143
+ - `mouse_click` - left/right/middle single or double click in desktop coordinates
144
+ - `keyboard_input` - Unicode text or named key/modifier chord
145
+
146
+ Desktop-Commander-compatible aliases:
147
+ - `start_process`
148
+ - `read_process_output`
149
+ - `interact_with_process`
150
+ - `list_sessions`
151
+ - `force_terminate`
152
+
153
+ ## Administration
154
+
155
+ Administration is intentionally separate from MCP. Set the Cloudflare `ADMIN_TOKEN` secret, keep the same value in the local ignored `.dev.vars` on the administrator machine, and use:
156
+
157
+ ```
158
+ npm run admin -- state
159
+ npm run admin -- bootstrap default "Primary PC"
160
+ npm run admin -- create-user "Alice"
161
+ npm run admin -- create-agent "Work Laptop" work-laptop
162
+ npm run admin -- grant <userId> <agentId> read,write,terminal,process
163
+ npm run admin -- grant <userId> <agentId> desktop_read
164
+ npm run admin -- grant <userId> <agentId> desktop_read,desktop_control
165
+ npm run admin -- revoke <userId> <agentId>
166
+ npm run admin -- enable-user <userId> false
167
+ $env:CHAT_RELAY_PASSWORD="choose-a-password"; npm run admin -- set-login <userId> <login>
168
+ npm run admin -- enable-agent <agentId> false
169
+ npm run admin -- rotate-user <userId>
170
+ npm run admin -- rotate-agent <agentId>
171
+ ```
172
+
173
+ Create and rotate commands return the new raw token once. Store it on the corresponding client/agent; the registry retains only its hash.
174
+
175
+ The initial `bootstrap` migrates the legacy `CALLER_TOKEN` and `AGENT_TOKEN` secrets into an owner user and the default agent. To move that existing owner to OAuth without creating a duplicate account, set `CHAT_RELAY_PASSWORD` in the administrator shell and run `npm run admin -- set-login owner <login>`. This attaches login credentials to the same owner user, preserving its grants and agent ownership.
176
+
177
+ ## Worker routes
178
+
179
+ | Route | Authentication | Purpose |
180
+ | --- | --- | --- |
181
+ | `GET /health` | none | Worker health |
182
+ | `GET /.well-known/oauth-protected-resource[/mcp]` | none | MCP protected-resource metadata |
183
+ | `GET /.well-known/oauth-authorization-server` | none | OAuth authorization-server metadata |
184
+ | `POST /register` | none | Dynamic registration for public PKCE clients |
185
+ | `GET/POST /authorize` | login/password | OAuth authorization-code sign-in |
186
+ | `POST /token` | public client + PKCE/refresh token | Access/refresh token exchange |
187
+ | `POST /auth/device/start` | none | Start CLI device authorization |
188
+ | `GET /device?user_code=<code>` | none | Browser sign-in/approval page |
189
+ | `POST /auth/device/approve` | login/password + device code | Approve or create a user account |
190
+ | `POST /auth/device/token` | device code | Exchange approved device code for session/agent credentials |
191
+ | `GET /auth/me` | user Bearer token | Current user and permitted agents |
192
+ | `POST /auth/session/revoke` | user Bearer token | Revoke only the current user session |
193
+ | `POST /auth/logout` | user Bearer token | Revoke local session and owning agent credential |
194
+ | `POST /mcp` | OAuth Bearer token | Streamable HTTP MCP (normal path) |
195
+ | `POST /mcp?key=<user token>` | legacy user token | Transitional Streamable HTTP MCP compatibility |
196
+ | `GET /agent?agentId=<id>` | agent Bearer token | Local agent WebSocket |
197
+ | `GET /status?agentId=<id>` | user token | Agent online status |
198
+ | `POST /relay?agentId=<id>` | user token | Direct JSON relay with scope checks |
199
+ | `/admin/*` | admin Bearer token | User/agent/grant administration |
200
+
201
+ ## Windows desktop access (opt-in)
202
+
203
+ Desktop interaction is disabled by default and requires two independent gates:
204
+
205
+ 1. Enable the local agent with `chat-relay remote --desktop` or `DESKTOP_ENABLED=1`. The CLI persists this setting locally. Use `--no-desktop` to disable it again.
206
+ 2. Grant `desktop_read` for screenshots and/or `desktop_control` for mouse/keyboard input. Generic read/write/terminal/process scopes do not imply desktop access.
207
+
208
+ - `screenshot` captures the primary interactive Windows display, scales/compresses it to a bounded JPEG, and returns it as an MCP image block. The accompanying metadata contains image size, desktop origin/size, and scale factors for converting screenshot pixels to desktop coordinates.
209
+ - `mouse_click` accepts integer desktop x/y coordinates, button `left|right|middle`, and click count 1 or 2. Invalid/out-of-bounds input is rejected rather than coerced.
210
+ - `keyboard_input` accepts either Unicode `text` or one named `key` with optional Ctrl/Alt/Shift/Win modifiers. Text and key cannot be supplied together.
211
+ - Windows interactive sessions are the v1 target. Non-Windows agents return `unsupported_platform`; unavailable/locked/non-interactive desktops return a controlled session/capture/input error and do not crash the reconnect loop.
212
+ - Screenshot bytes, typed text, key chords, and click coordinates are not stored in recentCalls. Only action/timing/success metadata is retained there.
213
+ - This card does not add streaming video, OCR, remote-desktop viewer UI, clipboard sync, drag-and-drop, app-specific automation, or Session 0/service automation.
214
+
215
+ ## Public plugin review
216
+
217
+ Public ChatGPT onboarding uses the plain production `/mcp` URL and OAuth; the legacy `?key=` route is migration compatibility only. Submission/reviewer requirements, permission boundaries, privacy/retention behavior, domain-verification setup, positive/negative test cases, and clean-room demo steps are maintained in `docs/plugin-review.md`.
218
+
219
+ The OpenAI domain verification token is served at `/.well-known/openai-apps-challenge` when `OPENAI_APPS_CHALLENGE` is configured. Keep reviewer credentials and challenge values separate from production administrator secrets.
220
+ ## Runtime limits and controls
221
+
222
+ - Per-user durable rate/quota controls are available before local-agent dispatch, but both are disabled by default (`rateLimit=0`, `dailyCallQuota=0`). Set `USER_RATE_LIMIT_PER_WINDOW` and/or `USER_DAILY_CALL_QUOTA` to a positive value to enable them; `USER_RATE_WINDOW_SECONDS` controls the rate window (1-3600 seconds).
223
+ - Admins can inspect or override the effective policy with `GET/POST /admin/api/limits`; POST `{ "resetToDefaults": true }` returns to environment defaults. Rejections return HTTP 429 with `rate_limited` or `quota_exceeded`, `Retry-After`, and reset metadata, and are recorded as bounded usage events without dispatching agent work.
224
+ - Relay request/response message: 64 KiB. The local agent caps serialized responses below that transport ceiling and returns `response_too_large` instead of allowing a silent timeout.
225
+ - `read_file` defaults to a 32 KiB content budget (max 48 KiB) and exposes deterministic `nextOffset` continuation.
226
+ - `read_multiple_files` defaults to a 48 KiB aggregate budget (max 48 KiB). If not all requested files fit, use `nextIndex`; if an individual file is truncated, continue it with that entry's `nextOffset`.
227
+ - One-shot terminal command: max 20 seconds.
228
+ - Persistent terminal sessions: up to 8 running sessions per local agent.
229
+ - Terminal output buffer: bounded in memory; completed sessions retained for 30 minutes.
230
+ - Filesystem reads/writes are bounded and restricted to configured `ALLOWED_ROOTS`.
231
+ - Search skips common heavy directories such as `.git`, `node_modules`, `.gradle`, `.idea`, and `.wrangler`.
232
+ - The agent blocks a small set of high-risk system-management commands. This is defense in depth, not a security sandbox.
233
+
234
+ ## Audit, retention, and recovery
235
+
236
+ - Security/admin mutations are recorded in a dedicated `Audit` Durable Object with actor, action, target, result, and sanitized scalar metadata only. Passwords, tokens, cookies, CSRF values, commands, file content, and raw tool payloads are excluded.
237
+ - Raw usage events default to 30-day retention while daily usage aggregates are preserved independently. Set `USAGE_RAW_RETENTION_DAYS` to change raw-event retention.
238
+ - Audit events default to 180-day retention. Set `AUDIT_RETENTION_DAYS` to change that window.
239
+ - `GET /admin/api/audit` returns bounded audit history. `GET /admin/api/operations` exposes component health and last cleanup state. `POST /admin/api/operations/cleanup` performs bounded idempotent cleanup across registry auth transients, raw usage events, and audit history.
240
+ - Cleanup does not intentionally remove active users, grants, agents, live sessions, pending non-expired device authorization, or long-lived usage aggregates.
241
+ - Durable Object state is the production source of truth. Git/npm artifacts do not back it up. Before destructive migration or account transfer, export any operator-required identity/config state separately and treat Cloudflare account/Durable Object recovery controls as the infrastructure recovery boundary.
242
+ - Cleanup failures are surfaced through the operations endpoint and audit result instead of being silently treated as success.
243
+ ## Tests
244
+
245
+ The legacy relay integration test remains available:
246
+
247
+ ```
248
+ npm run dev:test
249
+ python test/integration.py
250
+ ```
251
+
252
+ The full MCP/multi-user smoke test expects a local Worker on port 8795 plus an attached local agent:
253
+
254
+ ```
255
+ node test/multiuser-smoke.mjs
256
+ ```
257
+
258
+ It verifies the MCP tool surface, agent routing, filesystem operations, search sessions, process listing, persistent terminals, scope enforcement, multi-agent selection, and token rotation.
259
+
260
+ Device/browser login is covered by:
261
+
262
+ ```bash
263
+ TEST_RELAY_URL=http://127.0.0.1:8796 npm run test:device-auth
264
+ ```
265
+
266
+ OAuth discovery, DCR, PKCE, token exchange, refresh rotation, audience binding, MCP Bearer auth, and legacy-key compatibility are covered by:
267
+
268
+ ```bash
269
+ TEST_RELAY_URL=http://127.0.0.1:8796 npm run test:oauth
270
+ ```
271
+
272
+ Per-user rate/quota enforcement, isolation, reset-window behavior, disabled policy, rejection observability, and pre-dispatch blocking are covered by:
273
+
274
+ ```bash
275
+ TEST_RELAY_URL=http://127.0.0.1:8804 npm run test:quota
276
+ ```
277
+
278
+ Desktop tool contracts, scope separation, disabled/unsupported gates, controlled image mapping, and input validation are covered by:
279
+
280
+ ```bash
281
+ TEST_RELAY_URL=http://127.0.0.1:8807 npm run test:desktop
282
+ ```
283
+
284
+ After `npm pack`, the zero-checkout package smoke test installs and runs the tarball from a temporary directory:
285
+
286
+ ```bash
287
+ CHAT_RELAY_TARBALL=<path-to-tgz> TEST_RELAY_URL=http://127.0.0.1:8796 npm run test:zero-checkout
288
+ ```
289
+
290
+ The npm package is configured as `@anusornneal/chat-relay`. Publishing requires an authenticated npm account with access to that scope.
291
+
292
+ ## CI and npm publishing
293
+
294
+ GitHub Actions runs package verification on Node 20 and Node 24. A separate integration job starts a local Worker and verifies device auth, OAuth, and the zero-checkout tarball flow.
295
+
296
+ Before publishing locally:
297
+
298
+ ```bash
299
+ npm run verify:publish
300
+ ```
301
+
302
+ This checks the npm file set, CLI entrypoint, Worker dry-run build, and production dependency audit. The package whitelist excludes .dev.vars, Worker source, tests, and repository-only files.
303
+
304
+ The repository also includes a Publish npm workflow for version tags or manual dispatch. It supports npm trusted publishing through GitHub OIDC and can also use an NPM_TOKEN repository secret when configured.
305
+
306
+ The first registry publish still requires npm authorization for the @anusornneal scope. After publishing, verify the exact public UX from a clean directory:
307
+
308
+ ```bash
309
+ npx @anusornneal/chat-relay@latest status
310
+ npx @anusornneal/chat-relay@latest remote
311
+ ```