@oar-sh/oar 0.9.21 → 0.9.23
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/CHANGELOG.md +206 -0
- package/README.md +9 -9
- package/bin/oar.js +28 -0
- package/package.json +13 -14
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +9 -4
- package/server/claude-worker/claude-session-worker-main.mjs +10 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
- package/server/cursor-worker/cursor-session-worker.mjs +9 -1
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +1 -0
- package/server/grok-worker/grok-session-worker.mjs +10 -1
- package/server/grok-worker/grok-turn-runner.mjs +1 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +7 -0
- package/server/public/app/ask-user-view.js +4 -3
- package/server/public/app/bootstrap.js +62 -4
- package/server/public/app/chat-actions-menu-state.mjs +2 -0
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-view.js +306 -49
- package/server/public/app/header-priority-actions.mjs +5 -1
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/inbox-ui.js +70 -79
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +17 -0
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/session-history-ui.js +1 -0
- package/server/public/app/settings-modal.js +6 -0
- package/server/public/app/socket-handlers.js +42 -11
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +8 -6
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/index.html +391 -214
- package/server/repositories/message-repository.mjs +10 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/messages-routes.mjs +46 -34
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +77 -6
- package/server/server-runtime.mjs +199 -171
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/earlier-database-service.mjs +1 -1
- package/server/services/file-serving.mjs +13 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +7 -4
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +143 -15
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/update-install-service.mjs +32 -1
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +26 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,212 @@
|
|
|
3
3
|
All notable changes to OAR are documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
|
|
5
5
|
|
|
6
|
+
## [0.9.23] — 2026-11-07
|
|
7
|
+
|
|
8
|
+
Highlights: a dependency round. The Copilot, Claude and Cursor SDKs, zod,
|
|
9
|
+
express, socket.io, ws and better-sqlite3 move to newer releases:
|
|
10
|
+
the tree holds one zod, without CVE-2023-54404, and one express, and
|
|
11
|
+
prebuild-install, which is no longer maintained, is gone. OAR's package has
|
|
12
|
+
no install script any more, which package scanners flag. On Termux, 32-bit
|
|
13
|
+
ARM and FreeBSD, run **`oar build-native`** once after updating to this
|
|
14
|
+
version: it builds the SQLite binding, and later updates build it
|
|
15
|
+
themselves.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`oar build-native`.** better-sqlite3 13 ships its SQLite binding for
|
|
20
|
+
Linux, macOS and Windows on x64 and arm64. On Android under Termux, 32-bit
|
|
21
|
+
ARM and FreeBSD, `oar build-native` compiles it in the installed package
|
|
22
|
+
(Python 3, make and a C++ compiler; on Termux `pkg install python clang
|
|
23
|
+
make`). The **Update** button and `oar update` run it there themselves
|
|
24
|
+
before the relay restarts; a build that fails ends the update in an error
|
|
25
|
+
and leaves the running relay alone. Elsewhere it has nothing to do, so an
|
|
26
|
+
installer may run it everywhere. A relay started without the binding says
|
|
27
|
+
which command to run and exits instead of printing a stack trace.
|
|
28
|
+
- **What the agent SDKs contact.** docs/providers.md lists the services the
|
|
29
|
+
Copilot, Claude and Cursor SDKs and their CLIs talk to, and what the
|
|
30
|
+
Statsig client inside the Cursor SDK does.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **No install script.** OAR's package has no `preinstall` any more. From
|
|
35
|
+
0.9.14 to 0.9.22 it copied the data a relay before 0.9.14 kept inside the
|
|
36
|
+
package out before npm deleted it. npm 12 never ran it without
|
|
37
|
+
`--allow-scripts`, and package scanners flag every package that has one.
|
|
38
|
+
A copy those versions made is still taken over at the next start.
|
|
39
|
+
- **`--allow-scripts=koffi`.** better-sqlite3 13 has no install script, so
|
|
40
|
+
a manual install with npm 12 is now
|
|
41
|
+
`npm install -g --allow-scripts=koffi @oar-sh/oar`; the installers, the
|
|
42
|
+
**Update** button and `oar update` pass it themselves.
|
|
43
|
+
- **No terminal console.** The optional `tty-console` dependency is gone,
|
|
44
|
+
and neo-blessed with it. A relay started in a terminal prints its log
|
|
45
|
+
lines, each with its time, as under a service; routine worker lines are
|
|
46
|
+
no longer held back.
|
|
47
|
+
- **Express 5.** The file routes take express 5's syntax and serve the
|
|
48
|
+
same paths. Static JavaScript is served as `text/javascript`.
|
|
49
|
+
- `better-sqlite3` 13.0.3 (was 12.11.1); `prebuild-install`, which is no
|
|
50
|
+
longer maintained, and its subtree are gone. New in the tree:
|
|
51
|
+
`node-addon-api` 8.9.2, the headers of the source build.
|
|
52
|
+
- `express` 5.3.0 (was 4.22.3), the major the MCP SDK already used: one
|
|
53
|
+
express instead of two, and array-flatten, mime 1 and the rest of express
|
|
54
|
+
4's subtree are gone.
|
|
55
|
+
- `@anthropic-ai/claude-agent-sdk` 0.3.319 (was 0.3.304; bundled Claude
|
|
56
|
+
Code 2.1.319). The key chats (DeepSeek, Claude API) also pin the
|
|
57
|
+
cloud-sessions token and the two new switches that would let a folder's
|
|
58
|
+
settings widen which certificates Claude Code trusts.
|
|
59
|
+
- `@cursor/sdk` 1.3.1 (was 1.0.38). Its context-compaction summaries are
|
|
60
|
+
not shown yet.
|
|
61
|
+
- `@github/copilot-sdk` 1.0.29 (was 1.0.23; bundled Copilot CLI 1.0.107)
|
|
62
|
+
and `zod` 4.6.7 (was 4.4.3): one zod in the tree, without
|
|
63
|
+
CVE-2023-54404 (GHSA-mrhx-c7xc-m3w5).
|
|
64
|
+
- `socket.io` 4.8.4 (was 4.8.3) and `ws` 8.22.1 (was 8.21.3). `uuid` is
|
|
65
|
+
gone: Node's `crypto.randomUUID()` makes the same ids.
|
|
66
|
+
- package.json names its author and its bug tracker.
|
|
67
|
+
|
|
68
|
+
### Upgrade notes
|
|
69
|
+
|
|
70
|
+
- **From 0.9.13 or older, copy the data out of the package first.** A relay
|
|
71
|
+
before 0.9.14 could keep its database, uploads and config inside the
|
|
72
|
+
package folder, which `npm install -g` deletes, and this version no longer
|
|
73
|
+
copies them out. Copy `data/`, `uploads/` and `config.json` from the
|
|
74
|
+
installed package's `server` folder into a new folder under
|
|
75
|
+
`rescued-package-data` in the data folder (for example
|
|
76
|
+
`~/.oar/data/rescued-package-data/before-update/`); the first start takes
|
|
77
|
+
them over. From 0.9.14 on there is nothing to do.
|
|
78
|
+
- **Termux, 32-bit ARM, FreeBSD: the update from 0.9.22 needs
|
|
79
|
+
`oar build-native` once.** The Update button and `oar update` of 0.9.22
|
|
80
|
+
install 0.9.23 but do not build the binding, so the relay stops at its
|
|
81
|
+
first start on 0.9.23: its log names the command, and the page cannot
|
|
82
|
+
reach the relay. In Termux run:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
pkg install python clang make
|
|
86
|
+
oar build-native
|
|
87
|
+
oar start
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Later updates build the binding themselves, before the relay restarts.
|
|
91
|
+
- **Linux needs glibc 2.34 or newer,** for example Debian 12, Ubuntu 22.04
|
|
92
|
+
or RHEL 9. The SQLite binding better-sqlite3 13 ships for Linux on x64
|
|
93
|
+
and arm64 needs it, and a binding built with `oar build-native` would not
|
|
94
|
+
be used there. On an older system a relay that updates to 0.9.23 stays
|
|
95
|
+
down at its first start and says to update the operating system; stay on
|
|
96
|
+
0.9.22 until the system is updated.
|
|
97
|
+
- **No migration.** Going back to 0.9.22 needs no database step.
|
|
98
|
+
|
|
99
|
+
## [0.9.22] — 2026-11-05
|
|
100
|
+
|
|
101
|
+
Highlights: questions that don't block move into the chat header. A **📓**
|
|
102
|
+
icon with a count opens them, urgent ones first, and the turn that asked
|
|
103
|
+
keeps one line, "📓 Question for you", in place of the card. Settings →
|
|
104
|
+
General reads as five headed sections, with three new choices: whether
|
|
105
|
+
thoughts start open while an agent works, how many tool lines a working
|
|
106
|
+
agent shows, counted per agent so a busy sub-agent no longer pushes out the
|
|
107
|
+
main agent's lines, and a stream view that shows a turn's thoughts and tool
|
|
108
|
+
calls in the order they happened. While a turn runs, the page's poll leaves
|
|
109
|
+
out what it already holds, and an idle relay and its workers wake far less
|
|
110
|
+
often. Claude sessions now ask you in a **Safety check** card, which only
|
|
111
|
+
you can answer, before removals and other asks Claude Code marks as
|
|
112
|
+
person-only.
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- **📓 Questions in the chat header.** A conversation's questions that don't
|
|
117
|
+
block sit behind a 📓 icon with a count, first among the header icons (in
|
|
118
|
+
︙ when the header runs out of room). It opens a list under it with the
|
|
119
|
+
full cards, urgent ones first with their details open, and what was
|
|
120
|
+
answered in the last day below; answering keeps the list open. On a phone
|
|
121
|
+
the list spans the width over a dimmed page, with half-width buttons. While
|
|
122
|
+
an urgent question waits, 📓 and the Inbox show the urgent count in red.
|
|
123
|
+
The Inbox's link to a session opens it with 📓 at that question. Each ask
|
|
124
|
+
now remembers the turn that asked it (migration 0037).
|
|
125
|
+
- **A question leaves one line in the turn that asked it.** The cards of
|
|
126
|
+
questions that don't block no longer sit at the end of the conversation;
|
|
127
|
+
the reply of the turn that asked shows a one-line "📓 Question for you"
|
|
128
|
+
instead (live while it runs, "Answered: …" once answered), and a tap opens
|
|
129
|
+
📓 at that question. A question asked for another session, carried over by
|
|
130
|
+
a compaction or from before this version stands as one line at the end
|
|
131
|
+
while it waits.
|
|
132
|
+
- **Settings → General in headed sections.** The tab reads as five groups,
|
|
133
|
+
Appearance, Reading, Turns and tasks, This relay and Updates, instead of
|
|
134
|
+
one long column. Each relay-wide row now says it is a setting of this
|
|
135
|
+
relay; the per-browser rows keep "Saved in this browser."
|
|
136
|
+
- **Thoughts open or closed by your choice.** Settings → General →
|
|
137
|
+
Appearance has "Expand thoughts while an agent works" (on by default), with
|
|
138
|
+
a 💭 Thoughts mirror in the 🧠 modal. It decides whether thoughts start open
|
|
139
|
+
in the live turn, in live sub-agent bubbles and in the sub-agent viewer,
|
|
140
|
+
finished thoughts there included. An open live thought keeps a one-line
|
|
141
|
+
header to fold it by; a thought you fold or open by hand stays that way
|
|
142
|
+
while more text streams in. A finished reply's 💭 fold stays closed as
|
|
143
|
+
before.
|
|
144
|
+
- **The main agent's tool lines stay while sub-agents work.** The live
|
|
145
|
+
bubble's tool list is capped per agent now, the main agent and each
|
|
146
|
+
sub-agent apart, so a busy sub-agent no longer pushes the main agent's
|
|
147
|
+
lines out. Settings → General → Appearance has "Tool lines shown while an
|
|
148
|
+
agent works": 50 by default, 10 to 500 or no limit. The relay sends the
|
|
149
|
+
running turn's newest lines per agent as the page's setting asks (50 when
|
|
150
|
+
it does not say, 500 at most), and for a finished turn the main agent's
|
|
151
|
+
newest 200 and each sub-agent's newest 50 (it sent the first 48 of the
|
|
152
|
+
whole turn). A sub-agent bubble that holds as many lines as the setting
|
|
153
|
+
shows "50+ steps".
|
|
154
|
+
- **Stream view: a turn in the order it happened.** Settings → General →
|
|
155
|
+
Appearance has "Stream view" (off by default), with a 🧵 mirror in the 🧠
|
|
156
|
+
modal. When it is on, an agent turn's thoughts and tool calls show as one
|
|
157
|
+
list in the order they happened, live and in the finished message, with
|
|
158
|
+
the reply last. For Copilot, Cursor and Grok, tools and thoughts stream in
|
|
159
|
+
arrival order and the reply comes after them. The relay now numbers a
|
|
160
|
+
turn's thoughts and tool lines in one sequence (migration 0038); turns
|
|
161
|
+
recorded before that keep the grouped layout. A finished turn whose main
|
|
162
|
+
agent ran more than 200 tool lines says how many earlier ones it left out.
|
|
163
|
+
- **The live poll leaves out what the page already has.** While a turn
|
|
164
|
+
runs, the page asks for its conversation about once a second. It now
|
|
165
|
+
names the finished replies whose tool lines and thoughts it already holds,
|
|
166
|
+
and the relay leaves those out unless they changed since (a busy window:
|
|
167
|
+
about 6 KB per poll instead of over 200 KB). Opening a conversation,
|
|
168
|
+
paging back and every other load still get everything.
|
|
169
|
+
- **An idle relay and its workers wake far less often.** The relay no
|
|
170
|
+
longer polls its queue twice a second: a write that makes a message
|
|
171
|
+
deliverable wakes delivery at once, through triggers on the queue table.
|
|
172
|
+
A retry or a usage-limit hold gets a timer for the moment it ends, and a
|
|
173
|
+
10 s poll stays as a backstop. A gate lifting (relay resumed, restart
|
|
174
|
+
cancelled, tunnel connected, a kill's grace over, a worker replacement
|
|
175
|
+
done) wakes delivery too. The CLI status, the share-viewer prune and the
|
|
176
|
+
worker prime run only while there is something for them to do. Each
|
|
177
|
+
worker has one keepalive instead of two 10 s timers, and it beats every
|
|
178
|
+
30 s while idle, on a relay that supports it. A silent worker now counts
|
|
179
|
+
as gone after 90 s instead of 30 s. Workers and relays of older versions
|
|
180
|
+
keep working with the new ones.
|
|
181
|
+
- **Claude sessions ask you before removals and other asks Claude Code
|
|
182
|
+
marks as person-only, instead of allowing them.** Such an ask, for
|
|
183
|
+
example an `rm -rf` of the working folder, used to be approved by the
|
|
184
|
+
relay like every other ask. Now a **Safety check** card, in the session
|
|
185
|
+
and in the Inbox, shows Claude Code's reason
|
|
186
|
+
and then the whole command, each line behind `│`, with hidden or
|
|
187
|
+
reordering characters shown as `\u{…}`. It offers **Allow once** and
|
|
188
|
+
**Deny**, takes no typed reply, and only you can answer it: no agent,
|
|
189
|
+
of this relay or another, sees it among a session's questions or may
|
|
190
|
+
answer it. It allows only that one call and adds no rule. A denial, no
|
|
191
|
+
answer within the question timeout, a stopped turn or a relay that
|
|
192
|
+
cannot show the card all leave the command unrun, and a command too long
|
|
193
|
+
to show is refused without a card. Other asks are still allowed as
|
|
194
|
+
before.
|
|
195
|
+
|
|
196
|
+
### Upgrade notes
|
|
197
|
+
|
|
198
|
+
- **Two small migrations.** 0037 adds the asking turn to each ask and 0038
|
|
199
|
+
the sequence number to a turn's thoughts and tool lines, with an index;
|
|
200
|
+
neither rewrites the rows already there. As before, the relay copies its
|
|
201
|
+
database to the backups folder before the first start of a new version.
|
|
202
|
+
- **Every session worker is replaced once after the update,** as in
|
|
203
|
+
0.9.21: once its session is idle and its CLI has ended, usually some 10
|
|
204
|
+
minutes after its last turn. A Claude session shows the **Safety check**
|
|
205
|
+
card once its worker runs 0.9.22. Nothing needs to be done.
|
|
206
|
+
- **Mixed versions work together.** A 0.9.21 worker on a 0.9.22 relay keeps
|
|
207
|
+
its 10 s beat, and a 0.9.22 worker on a 0.9.21 relay beats every 10 s.
|
|
208
|
+
Paired relays of both versions keep working with each other.
|
|
209
|
+
- **No new dependency.** Going back to 0.9.21 needs no database step: it
|
|
210
|
+
leaves the new columns and the index alone.
|
|
211
|
+
|
|
6
212
|
## [0.9.21] — 2026-11-01
|
|
7
213
|
|
|
8
214
|
Highlights: the chat screen is more compact. The mode, model, effort,
|
package/README.md
CHANGED
|
@@ -45,29 +45,29 @@ flowchart LR
|
|
|
45
45
|
<table>
|
|
46
46
|
<tr>
|
|
47
47
|
<td width="42%" rowspan="4" valign="top">
|
|
48
|
-
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.
|
|
48
|
+
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.23" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
|
|
49
49
|
<br><sub>On a phone: the same conversations, installable as an app.</sub>
|
|
50
50
|
</td>
|
|
51
51
|
<td width="58%" valign="top">
|
|
52
|
-
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.
|
|
52
|
+
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.23" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
|
|
53
53
|
<br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
|
|
54
54
|
</td>
|
|
55
55
|
</tr>
|
|
56
56
|
<tr>
|
|
57
57
|
<td valign="top">
|
|
58
|
-
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.
|
|
58
|
+
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.23" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
|
|
59
59
|
<br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
|
|
60
60
|
</td>
|
|
61
61
|
</tr>
|
|
62
62
|
<tr>
|
|
63
63
|
<td valign="top">
|
|
64
|
-
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.
|
|
64
|
+
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.23" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
|
|
65
65
|
<br><sub>Read the reasoning, not just the answer.</sub>
|
|
66
66
|
</td>
|
|
67
67
|
</tr>
|
|
68
68
|
<tr>
|
|
69
69
|
<td valign="top">
|
|
70
|
-
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.
|
|
70
|
+
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.23" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
|
|
71
71
|
<br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
|
|
72
72
|
</td>
|
|
73
73
|
</tr>
|
|
@@ -118,11 +118,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
118
118
|
<table>
|
|
119
119
|
<tr>
|
|
120
120
|
<td width="62%" valign="top">
|
|
121
|
-
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.
|
|
121
|
+
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.23" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
|
|
122
122
|
<br><sub>Send a message while the agent works, and it steers the running turn.</sub>
|
|
123
123
|
</td>
|
|
124
124
|
<td width="38%" valign="top">
|
|
125
|
-
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.
|
|
125
|
+
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.23" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
|
|
126
126
|
<br><sub>What each plan has left, in one place.</sub>
|
|
127
127
|
</td>
|
|
128
128
|
</tr>
|
|
@@ -139,7 +139,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
139
139
|
- **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
|
|
140
140
|
- **On your phone**: install OAR as an app and get push notifications; back walks through the conversations you opened, a pinch zooms the whole app, and held sideways it keeps a phone's layout ([Notifications and the app](docs/web-ui.md#notifications-and-the-app), [back and forward](docs/web-ui.md#back-and-forward), [zoom](docs/web-ui.md#zoom), [a phone held sideways](docs/web-ui.md#a-phone-held-sideways)).
|
|
141
141
|
|
|
142
|
-
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.
|
|
142
|
+
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.23](CHANGELOG.md#0923--2026-11-07) as the latest.
|
|
143
143
|
|
|
144
144
|
## Remote access
|
|
145
145
|
|
|
@@ -190,7 +190,7 @@ flowchart LR
|
|
|
190
190
|
|
|
191
191
|
You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
|
|
192
192
|
|
|
193
|
-
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.
|
|
193
|
+
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.23" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
|
|
194
194
|
|
|
195
195
|
How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
|
|
196
196
|
|
package/bin/oar.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../server/services/oar-state-migration-service.mjs';
|
|
15
15
|
import { readConfiguredDataDir, resolveDataDir, resolveUploadDir } from '../server/services/data-dir-resolution.mjs';
|
|
16
16
|
import { RESCUE_DIR_NAME, TAKEOVER_PROGRESS_FILE } from '../server/services/earlier-database-service.mjs';
|
|
17
|
+
import { NATIVE_BUILD_COMMAND, buildBetterSqlite3, needsSourceBuild } from '../server/services/native-addon-build-service.mjs';
|
|
17
18
|
import { DB_FILE_NAME, DB_FILE_NAMES, LEGACY_DB_FILE_NAME, resolveDatabaseFile } from '../server/services/database-file.mjs';
|
|
18
19
|
import { BACKUP_PROGRESS_FILE, BACKUP_WAIT_FILE, backupDatabase, configWarnings, describeBackups, sanitizeReason } from '../server/services/db-backup-service.mjs';
|
|
19
20
|
import { defaultIsProcessAlive, lockHolderMayRun, processIdentity } from '../server/services/relay-singleton-guard.mjs';
|
|
@@ -2407,6 +2408,15 @@ async function runUpdate({ argv = [], env = process.env, logger = console, fetch
|
|
|
2407
2408
|
return { code: 1, reason: 'npm-failed' };
|
|
2408
2409
|
}
|
|
2409
2410
|
logger.log(`[oar] Installed @oar-sh/oar@${target}.`);
|
|
2411
|
+
// No prebuilt SQLite binding here: build it before the relay restarts on
|
|
2412
|
+
// the new version (native-addon-build-service.mjs).
|
|
2413
|
+
if (needsSourceBuild()) {
|
|
2414
|
+
const build = await buildBetterSqlite3({ packageRoot, env: npm.env, spawnImpl, logger, onOutput: (text) => process.stdout.write(text) });
|
|
2415
|
+
if (!build.ok) {
|
|
2416
|
+
logger.error(`[oar] The relay keeps running the version it has loaded. Run ${NATIVE_BUILD_COMMAND}, then restart it.`);
|
|
2417
|
+
return { code: 1, reason: 'native-build-failed' };
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2410
2420
|
|
|
2411
2421
|
// A running relay picks the new code up on restart; ask for the queue-idle
|
|
2412
2422
|
// deferred one so no in-flight turn is cut off.
|
|
@@ -2446,6 +2456,23 @@ async function runUpdate({ argv = [], env = process.env, logger = console, fetch
|
|
|
2446
2456
|
return { code: 0, reason: 'updated' };
|
|
2447
2457
|
}
|
|
2448
2458
|
|
|
2459
|
+
/**
|
|
2460
|
+
* `oar build-native [--force]`: the SQLite binding of better-sqlite3 where no
|
|
2461
|
+
* prebuilt one ships (Termux, 32-bit ARM, FreeBSD); elsewhere nothing to do,
|
|
2462
|
+
* so an installer may run it everywhere. `--force` builds anyway, which only
|
|
2463
|
+
* tests the toolchain where a prebuilt binding ships: better-sqlite3 loads
|
|
2464
|
+
* that one first.
|
|
2465
|
+
*/
|
|
2466
|
+
async function runBuildNative({ argv = [], env = process.env, logger = console, spawnImpl = spawn } = {}) {
|
|
2467
|
+
const force = argv.includes('--force');
|
|
2468
|
+
if (!force && !needsSourceBuild()) {
|
|
2469
|
+
logger.log(`[oar] better-sqlite3 ships a prebuilt SQLite binding for ${process.platform}-${process.arch}; nothing to build.`);
|
|
2470
|
+
return { code: 0, reason: 'prebuilt' };
|
|
2471
|
+
}
|
|
2472
|
+
const result = await buildBetterSqlite3({ packageRoot: resolvePackageRoot(), force, env, spawnImpl, logger, onOutput: (text) => process.stdout.write(text) });
|
|
2473
|
+
return result.ok ? { code: 0, reason: 'built' } : { code: 1, reason: 'build-failed' };
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2449
2476
|
/** `oar` without a command: the menu in a terminal, the status and the usage elsewhere. */
|
|
2450
2477
|
async function runMenuCommand(options = {}) {
|
|
2451
2478
|
const ctx = cliContext(options);
|
|
@@ -2495,6 +2522,7 @@ const COMMANDS = {
|
|
|
2495
2522
|
doctor: runDoctor,
|
|
2496
2523
|
backup: runBackup,
|
|
2497
2524
|
update: runUpdate,
|
|
2525
|
+
'build-native': runBuildNative,
|
|
2498
2526
|
help: async ({ logger = console } = {}) => {
|
|
2499
2527
|
logger.log(usageText());
|
|
2500
2528
|
return { code: 0, reason: 'help' };
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oar-sh/oar",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"author": "Simon",
|
|
7
8
|
"homepage": "https://oar.sh",
|
|
8
9
|
"repository": {
|
|
9
10
|
"type": "git",
|
|
10
11
|
"url": "git+https://github.com/oar-sh/oar.git"
|
|
11
12
|
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/oar-sh/oar/issues"
|
|
15
|
+
},
|
|
12
16
|
"engines": {
|
|
13
17
|
"node": ">=22.13"
|
|
14
18
|
},
|
|
@@ -41,7 +45,6 @@
|
|
|
41
45
|
"LICENSE"
|
|
42
46
|
],
|
|
43
47
|
"scripts": {
|
|
44
|
-
"preinstall": "node server/services/package-data-rescue-preinstall.mjs",
|
|
45
48
|
"start": "node server/server.js",
|
|
46
49
|
"copilot:relay": "gh copilot -- --allow-all",
|
|
47
50
|
"test": "node --import ./scripts/node-test-keep-alive.mjs --import ./scripts/node-test-worktree-guard.mjs --test \"server/**/*.test.mjs\" \"shared/**/*.test.mjs\" \".github/extensions/web-relay/**/*.test.mjs\" \"scripts/**/*.test.mjs\"",
|
|
@@ -55,20 +58,16 @@
|
|
|
55
58
|
"test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
|
|
56
59
|
},
|
|
57
60
|
"dependencies": {
|
|
58
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
59
|
-
"@cursor/sdk": "1.
|
|
60
|
-
"@github/copilot-sdk": "1.0.
|
|
61
|
-
"better-sqlite3": "
|
|
62
|
-
"express": "
|
|
61
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.319",
|
|
62
|
+
"@cursor/sdk": "1.3.1",
|
|
63
|
+
"@github/copilot-sdk": "1.0.29",
|
|
64
|
+
"better-sqlite3": "13.0.3",
|
|
65
|
+
"express": "5.3.0",
|
|
63
66
|
"qrcode-terminal": "0.12.0",
|
|
64
|
-
"socket.io": "4.8.
|
|
65
|
-
"uuid": "14.0.1",
|
|
67
|
+
"socket.io": "4.8.4",
|
|
66
68
|
"web-push": "3.6.7",
|
|
67
|
-
"ws": "8.
|
|
68
|
-
"zod": "4.
|
|
69
|
-
},
|
|
70
|
-
"optionalDependencies": {
|
|
71
|
-
"tty-console": "1.0.4"
|
|
69
|
+
"ws": "8.22.1",
|
|
70
|
+
"zod": "4.6.7"
|
|
72
71
|
},
|
|
73
72
|
"devDependencies": {
|
|
74
73
|
"@playwright/test": "^1.60.0",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
|
|
15
15
|
import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
|
|
16
16
|
import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
|
|
17
|
+
import { createWorkerKeepalive } from '../../shared/worker-runtime/worker-keepalive.mjs';
|
|
17
18
|
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
18
19
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
19
20
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
@@ -124,8 +125,13 @@ async function main() {
|
|
|
124
125
|
getSteeringState: () => buildClaudeCloudSteeringState(runner),
|
|
125
126
|
});
|
|
126
127
|
|
|
128
|
+
// One timer for the heartbeat and the socket's readiness refresh: 10 s
|
|
129
|
+
// while working, 30 s while idle on a relay that allows it.
|
|
130
|
+
let keepalive = null;
|
|
127
131
|
const wsLink = createWorkerWebSocketLink({
|
|
128
132
|
serverUrl,
|
|
133
|
+
readyRefreshMs: 0,
|
|
134
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
129
135
|
token,
|
|
130
136
|
dbg,
|
|
131
137
|
buildId,
|
|
@@ -142,10 +148,12 @@ async function main() {
|
|
|
142
148
|
}
|
|
143
149
|
},
|
|
144
150
|
});
|
|
151
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
145
152
|
|
|
146
153
|
const shutdown = async (signal) => {
|
|
147
154
|
dbg(`shutting down (${signal})`);
|
|
148
155
|
try { wsLink.stop(); } catch {}
|
|
156
|
+
try { keepalive.stop(); } catch {}
|
|
149
157
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
150
158
|
try { controlPoller.stop(); } catch {}
|
|
151
159
|
// The cloud session is left as it is: a turn in flight keeps running
|
|
@@ -162,7 +170,7 @@ async function main() {
|
|
|
162
170
|
});
|
|
163
171
|
|
|
164
172
|
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
165
|
-
|
|
173
|
+
keepalive.start();
|
|
166
174
|
wsLink.start();
|
|
167
175
|
// Not awaited: deliveries wait for it inside the runner, and a relay that
|
|
168
176
|
// cannot say which cloud session this is leaves the binding to the first
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
|
|
23
23
|
* store, so those two are pinned only when the worker sets them.
|
|
24
24
|
*
|
|
25
|
-
* Source: the names Claude Code 2.1.
|
|
25
|
+
* Source: the names Claude Code 2.1.319 (the bundled CLI) reads, by a grep
|
|
26
26
|
* of the binary; the CLI's own lists of endpoints (with their selection
|
|
27
27
|
* switches and companions) and credentials are in it as they are there. A
|
|
28
28
|
* deny-list, because a settings layer can only pin names known in advance.
|
|
@@ -82,6 +82,7 @@ export const CLAUDE_CLI_PINNED_ENV = Object.freeze([
|
|
|
82
82
|
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
83
83
|
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
84
84
|
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
85
|
+
'CLAUDE_CODE_SESSIONS_OAUTH_TOKEN',
|
|
85
86
|
'ANTHROPIC_PROFILE',
|
|
86
87
|
'ANTHROPIC_IDENTITY_TOKEN',
|
|
87
88
|
'ANTHROPIC_IDENTITY_TOKEN_FILE',
|
|
@@ -106,6 +107,8 @@ export const CLAUDE_CLI_PINNED_ENV = Object.freeze([
|
|
|
106
107
|
'NODE_TLS_REJECT_UNAUTHORIZED',
|
|
107
108
|
'NODE_EXTRA_CA_CERTS',
|
|
108
109
|
'CLAUDE_CODE_CERT_STORE',
|
|
110
|
+
'CLAUDE_CODE_DISABLE_LOOPED_CROSS_CERT_DROP',
|
|
111
|
+
'CLAUDE_CODE_DISABLE_SELF_NAMED_CERT_DROP',
|
|
109
112
|
'CLAUDE_CODE_CLIENT_CERT',
|
|
110
113
|
'CLAUDE_CODE_CLIENT_KEY',
|
|
111
114
|
'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
|
|
@@ -2,6 +2,7 @@ import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
2
2
|
|
|
3
3
|
import { formatClaudeBackgroundCommandLimitLabel } from '../../shared/claude-background-command-limit.mjs';
|
|
4
4
|
import { parseThinkingDisplay } from '../../shared/claude-thinking.mjs';
|
|
5
|
+
import { PERSON_ONLY_QUESTION_SOURCE } from '../../shared/relay-asks.mjs';
|
|
5
6
|
import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructions.mjs';
|
|
6
7
|
import { REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS } from '../../shared/remote-relay-tool-core.mjs';
|
|
7
8
|
import { createPreviewToolDefinition } from './claude-preview-tool.mjs';
|
|
@@ -631,11 +632,151 @@ export async function readPlanUsage(turn, dbg = () => {}, { timeoutMs = 10000 }
|
|
|
631
632
|
}
|
|
632
633
|
}
|
|
633
634
|
|
|
635
|
+
// Asks Claude Code marks as person-only: a removal of a workspace or system
|
|
636
|
+
// directory, a target it cannot resolve. No rule, mode or automatic approval
|
|
637
|
+
// may answer them, so allow-all parity stops here. The SDK does not forward
|
|
638
|
+
// the wire's `classifier_approvable`, so the safety-check kinds count only
|
|
639
|
+
// together with `suppressAlwaysAllowRule`; a sensitive-file safety check
|
|
640
|
+
// (an edit under .git/ or .claude/) carries no such flag and stays allowed.
|
|
641
|
+
// A user's `permissions.ask` rule (`matchedAskRule`) changes nothing: the
|
|
642
|
+
// flags decide, and an ask that rule forced without them stays allowed.
|
|
643
|
+
const PERSON_ONLY_REASON_TYPES = new Set(['safetyCheck', 'subcommandResults']);
|
|
644
|
+
export const PERSON_ONLY_ALLOW_CHOICE = 'Allow once';
|
|
645
|
+
export const PERSON_ONLY_DENY_CHOICE = 'Deny';
|
|
646
|
+
export { PERSON_ONLY_QUESTION_SOURCE };
|
|
647
|
+
// The card's first line is fixed text: the relay's console log prints the
|
|
648
|
+
// first 60 characters of a question, and the command must not reach it.
|
|
649
|
+
const PERSON_ONLY_QUESTION_LEAD = 'Claude Code marks this as person-only: no rule or automatic approval may allow it. Allow it once?';
|
|
650
|
+
const PERSON_ONLY_RATIONALE = 'Claude Code needs a person to approve this before it runs. Nothing runs until you choose.';
|
|
651
|
+
// The card shows the whole command or nothing: a cut could hide the removal.
|
|
652
|
+
export const PERSON_ONLY_COMMAND_MAX = 8000;
|
|
653
|
+
export const PERSON_ONLY_TOO_LONG_MESSAGE = 'Not run: too long to show for approval; issue the removal as a short command of its own.';
|
|
654
|
+
const PERSON_ONLY_REASON_MAX = 400;
|
|
655
|
+
const PERSON_ONLY_TYPED_ANSWER_MAX = 500;
|
|
656
|
+
|
|
657
|
+
export function isPersonOnlyAsk(options) {
|
|
658
|
+
if (!options || typeof options !== 'object') return false;
|
|
659
|
+
if (options.defaultToNo === true) return true;
|
|
660
|
+
return PERSON_ONLY_REASON_TYPES.has(options.decisionReasonType)
|
|
661
|
+
&& options.suppressAlwaysAllowRule === true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Characters that hide or reorder text: C0 and C1 controls, bidi controls,
|
|
665
|
+
// zero-width characters and the Unicode line separators. On the card each
|
|
666
|
+
// one shows as a visible \u{XXXX} escape (the body is pre-wrap plain text).
|
|
667
|
+
const INVISIBLE_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g;
|
|
668
|
+
|
|
669
|
+
function escapeInvisible(text) {
|
|
670
|
+
return String(text ?? '').replace(INVISIBLE_CHARACTERS, (char) => (
|
|
671
|
+
`\\u{${char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}}`
|
|
672
|
+
));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// The CLI's reason may carry ANSI colour (sdk.d.ts): drop well-formed
|
|
676
|
+
// sequences, then escape what is left, newlines included (one line).
|
|
677
|
+
function reasonLine(value) {
|
|
678
|
+
const text = String(value ?? '')
|
|
679
|
+
.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
|
|
680
|
+
.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
681
|
+
.trim();
|
|
682
|
+
return capText(escapeInvisible(text), PERSON_ONLY_REASON_MAX);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function capText(text, max) {
|
|
686
|
+
if (text.length <= max) return text;
|
|
687
|
+
return `${text.slice(0, max)}… (${text.length - max} more characters)`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function personOnlySubject(input) {
|
|
691
|
+
if (typeof input?.command === 'string') return { label: 'command', text: input.command };
|
|
692
|
+
for (const key of ['file_path', 'notebook_path', 'path']) {
|
|
693
|
+
if (typeof input?.[key] === 'string') return { label: 'path', text: input[key] };
|
|
694
|
+
}
|
|
695
|
+
let json = '';
|
|
696
|
+
try { json = JSON.stringify(input ?? {}); } catch { json = ''; }
|
|
697
|
+
return { label: 'input', text: json };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Each line escaped and behind "│ ", each line break marked "⏎", so no line
|
|
701
|
+
// of the command can pass for the card's own text.
|
|
702
|
+
function subjectLines(text) {
|
|
703
|
+
return String(text ?? '').split('\n').map(escapeInvisible);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function subjectTooLong(input) {
|
|
707
|
+
return subjectLines(personOnlySubject(input).text).join('\n').length > PERSON_ONLY_COMMAND_MAX;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function personOnlyQuestionText(toolName, input, decisionReason) {
|
|
711
|
+
const subject = personOnlySubject(input);
|
|
712
|
+
const lines = subjectLines(subject.text);
|
|
713
|
+
const parts = [PERSON_ONLY_QUESTION_LEAD];
|
|
714
|
+
const reason = reasonLine(decisionReason);
|
|
715
|
+
if (reason) parts.push(`Reason: ${reason}`);
|
|
716
|
+
const count = lines.length > 1 ? `, ${lines.length} lines` : '';
|
|
717
|
+
parts.push(`${toolName || 'Tool'} ${subject.label}${count}:\n│ ${lines.join(' ⏎\n│ ')}`);
|
|
718
|
+
return parts.join('\n\n');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function personOnlyDenyMessage(decisionReason, why) {
|
|
722
|
+
const reason = reasonLine(decisionReason);
|
|
723
|
+
return `Not run: Claude Code marks this as person-only${reason ? ` (${reason})` : ''}, and ${why}. `
|
|
724
|
+
+ 'Do not work around it by splitting, scripting or re-issuing it through another tool.';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Allow/Deny card over the question bridge, no typed replies. Only the exact
|
|
728
|
+
// "Allow once" allows, and never with `updatedPermissions`: the next such ask
|
|
729
|
+
// asks again.
|
|
730
|
+
async function askPersonOnly({ askUserBridge, toolName, input, options, signal, dbg }) {
|
|
731
|
+
const reason = options?.decisionReason;
|
|
732
|
+
const kind = options?.decisionReasonType || 'defaultToNo';
|
|
733
|
+
const deny = (why, outcome) => {
|
|
734
|
+
dbg('person-only ask', toolName, kind, outcome);
|
|
735
|
+
return { behavior: 'deny', message: personOnlyDenyMessage(reason, why) };
|
|
736
|
+
};
|
|
737
|
+
if (signal?.aborted) return deny('the turn was stopped', 'aborted');
|
|
738
|
+
if (subjectTooLong(input)) {
|
|
739
|
+
dbg('person-only ask', toolName, kind, 'too-long');
|
|
740
|
+
return { behavior: 'deny', message: PERSON_ONLY_TOO_LONG_MESSAGE };
|
|
741
|
+
}
|
|
742
|
+
if (!askUserBridge) return deny('the relay could not show the question', 'no-bridge');
|
|
743
|
+
const question = personOnlyQuestionText(toolName, input, reason);
|
|
744
|
+
let result;
|
|
745
|
+
try {
|
|
746
|
+
result = await askUserBridge.handleAskUserQuestion({
|
|
747
|
+
questions: [{
|
|
748
|
+
question,
|
|
749
|
+
header: 'Safety check',
|
|
750
|
+
options: [{ label: PERSON_ONLY_ALLOW_CHOICE }, { label: PERSON_ONLY_DENY_CHOICE }],
|
|
751
|
+
}],
|
|
752
|
+
}, {
|
|
753
|
+
signal,
|
|
754
|
+
source: PERSON_ONLY_QUESTION_SOURCE,
|
|
755
|
+
rationale: PERSON_ONLY_RATIONALE,
|
|
756
|
+
allowFreeform: false,
|
|
757
|
+
});
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (signal?.aborted) return deny('the turn was stopped', 'aborted');
|
|
760
|
+
dbg('person-only ask bridge failed', error?.message || String(error));
|
|
761
|
+
return deny('the relay could not show the question', 'bridge-failed');
|
|
762
|
+
}
|
|
763
|
+
if (signal?.aborted) return deny('the turn was stopped', 'aborted');
|
|
764
|
+
if (result?.timedOut) return deny('no answer came before the timeout', 'timed-out');
|
|
765
|
+
const answer = String(result?.answers?.[question] ?? '').trim();
|
|
766
|
+
if (answer === PERSON_ONLY_ALLOW_CHOICE) {
|
|
767
|
+
dbg('person-only ask', toolName, kind, 'allowed');
|
|
768
|
+
return { behavior: 'allow', updatedInput: input };
|
|
769
|
+
}
|
|
770
|
+
if (!answer || answer === PERSON_ONLY_DENY_CHOICE) return deny('the user denied it', 'denied');
|
|
771
|
+
return deny(`the user denied it: ${capText(escapeInvisible(answer), PERSON_ONLY_TYPED_ANSWER_MAX)}`, 'denied-other');
|
|
772
|
+
}
|
|
773
|
+
|
|
634
774
|
/**
|
|
635
775
|
* Build the `canUseTool` callback for one turn. Everything is auto-allowed
|
|
636
|
-
* (parity with the Copilot workers' `--allow-all` posture)
|
|
637
|
-
*
|
|
638
|
-
*
|
|
776
|
+
* (parity with the Copilot workers' `--allow-all` posture) except the asks
|
|
777
|
+
* Claude Code marks as person-only (an Allow/Deny relay card, see
|
|
778
|
+
* isPersonOnlyAsk); the other interceptions are AskUserQuestion (bridged to
|
|
779
|
+
* relay question cards) and ExitPlanMode (surfaces the plan_ready board).
|
|
639
780
|
*
|
|
640
781
|
* ExitPlanMode is DENIED after the board is posted: allowing it tells the CLI
|
|
641
782
|
* the user approved the plan, and the same turn rolls straight into
|
|
@@ -658,8 +799,12 @@ export function createCanUseTool({
|
|
|
658
799
|
onExitPlanMode,
|
|
659
800
|
dbg = () => {},
|
|
660
801
|
} = {}) {
|
|
661
|
-
return async function canUseTool(toolName, input,
|
|
802
|
+
return async function canUseTool(toolName, input, options = {}) {
|
|
803
|
+
const signal = options?.signal;
|
|
662
804
|
const normalizedToolName = String(toolName || '').trim();
|
|
805
|
+
if (isPersonOnlyAsk(options)) {
|
|
806
|
+
return askPersonOnly({ askUserBridge, toolName: normalizedToolName, input, options, signal, dbg });
|
|
807
|
+
}
|
|
663
808
|
try {
|
|
664
809
|
if (normalizedToolName === 'AskUserQuestion' && askUserBridge) {
|
|
665
810
|
const { answers } = await askUserBridge.handleAskUserQuestion(input, { signal });
|