@oar-sh/oar 0.9.6 → 0.9.8
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 +285 -0
- package/README.md +222 -40
- package/bin/oar.js +320 -18
- package/package.json +1 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -13
- package/server/claude-worker/claude-session-process.mjs +225 -24
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
- package/server/db-schema.mjs +17 -0
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/activity-replay-state.mjs +68 -5
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +149 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +468 -88
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +11 -0
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +127 -8
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +145 -11
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +698 -28
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/pwa-install.js +5 -0
- package/server/public/app/relay-error-ctas.mjs +16 -2
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/scrollbar-hover.mjs +78 -0
- package/server/public/app/settings-modal.js +37 -3
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +53 -1
- package/server/public/app/store.js +26 -0
- package/server/public/app/transcript-separators.mjs +48 -11
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon-light-192.png +0 -0
- package/server/public/app-icon-light-512.png +0 -0
- package/server/public/app-icon-light-maskable-512.png +0 -0
- package/server/public/app-icon-light.svg +4 -0
- package/server/public/app-icon-maskable-512.png +0 -0
- package/server/public/app-icon.svg +3 -3
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +564 -20
- package/server/public/manifest.webmanifest +9 -3
- package/server/public/sw.js +6 -1
- package/server/relay-tools.md +13 -1
- package/server/repositories/remote-relay-repository.mjs +42 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +123 -16
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +517 -74
- package/server/server-runtime.mjs +268 -33
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/claude-context-usage.mjs +51 -3
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/oar-cli-helpers.mjs +67 -1
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/pwa-manifest-theme.mjs +30 -0
- package/server/services/remote-relay-caller-context.mjs +49 -10
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +383 -35
- package/server/services/remote-relay-inbound.mjs +49 -2
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +348 -129
- package/server/services/session-worker-registry-service.mjs +18 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- package/server/services/update-install-service.mjs +9 -3
- package/server/services/usage-limit-pause-service.mjs +3 -1
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/remote-relay-contract.mjs +153 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +1 -0
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
// provenance ("origin") shape a relay attaches when its agent prompts a remote
|
|
8
8
|
// session, the prompt header line, and the URL policy for remote addresses.
|
|
9
9
|
//
|
|
10
|
+
// Since agent sessions, the relay an agent runs on is a target as well (the
|
|
11
|
+
// "local target", addressed by this relay's name or `this`): the same tool
|
|
12
|
+
// creates and drives sessions on its own relay, behind a setting.
|
|
13
|
+
//
|
|
10
14
|
// SDK-free and side-effect free: imported by the server, the session workers,
|
|
11
15
|
// the stdio MCP server and (for the header/URL helpers) the browser.
|
|
12
16
|
|
|
17
|
+
import { isValidBranchName, normalizeGitHubRepoUrl } from './claude-cloud/repo-url.mjs';
|
|
18
|
+
|
|
13
19
|
export const REMOTE_RELAY_TOOL_NAME = 'remote_relay';
|
|
14
20
|
|
|
15
21
|
// Bumped when a relay starts relying on a newer inbound behaviour of its peers.
|
|
@@ -60,7 +66,7 @@ export const REMOTE_RELAY_PROMPT_ACTIONS = Object.freeze(['send', 'create_sessio
|
|
|
60
66
|
export const REMOTE_RELAY_APPROVAL_MODES = Object.freeze(['ask', 'plan']);
|
|
61
67
|
|
|
62
68
|
export const REMOTE_RELAY_SESSION_SCOPES = Object.freeze(['active', 'recent', 'all']);
|
|
63
|
-
export const REMOTE_RELAY_PROVIDERS = Object.freeze(['github', 'openai', 'claude', 'cursor', 'grok']);
|
|
69
|
+
export const REMOTE_RELAY_PROVIDERS = Object.freeze(['github', 'openai', 'claude', 'cursor', 'grok', 'claude-cloud']);
|
|
64
70
|
export const REMOTE_RELAY_MODES = Object.freeze(['plan', 'ask', 'agent', 'autopilot']);
|
|
65
71
|
export const REMOTE_RELAY_IF_BUSY = Object.freeze(['queue', 'fail']);
|
|
66
72
|
// Reasoning efforts differ per provider and model (none, low, medium, high,
|
|
@@ -81,7 +87,16 @@ export const REMOTE_RELAY_RESULT_STATUSES = Object.freeze([
|
|
|
81
87
|
|
|
82
88
|
export const REMOTE_RELAY_LIMITS = Object.freeze({
|
|
83
89
|
waitDefaultSeconds: 120,
|
|
90
|
+
// The longest wait of one call is a setting of the relay ("Longest wait per
|
|
91
|
+
// tool call"): waitMaxSeconds is its default, the next three its range. The
|
|
92
|
+
// validator below clamps to the ceiling; the relay clamps to its setting.
|
|
84
93
|
waitMaxSeconds: 600,
|
|
94
|
+
waitMaxFloorSeconds: 120,
|
|
95
|
+
waitMaxCeilingSeconds: 3600,
|
|
96
|
+
waitMaxStepSeconds: 60,
|
|
97
|
+
// Sessions one conversation's agent started on its own relay that may have
|
|
98
|
+
// a turn queued or running at the same time.
|
|
99
|
+
localActiveSessions: 4,
|
|
85
100
|
listDefault: 20,
|
|
86
101
|
listMax: 100,
|
|
87
102
|
readDefault: 10,
|
|
@@ -119,6 +134,15 @@ export const REMOTE_RELAY_ERROR_CODES = Object.freeze({
|
|
|
119
134
|
providerUnavailable: 'REMOTE_RELAY_PROVIDER_UNAVAILABLE',
|
|
120
135
|
busy: 'REMOTE_RELAY_BUSY',
|
|
121
136
|
noTurn: 'REMOTE_RELAY_NO_ACTIVE_TURN',
|
|
137
|
+
// The local target (this relay itself):
|
|
138
|
+
// the setting "Agents may start and use sessions on this relay" is off,
|
|
139
|
+
localDisabled: 'REMOTE_RELAY_LOCAL_DISABLED',
|
|
140
|
+
// a session an agent created tried to create one itself,
|
|
141
|
+
nestedSession: 'REMOTE_RELAY_NESTED_SESSION',
|
|
142
|
+
// the conversation already has `localActiveSessions` created sessions at work,
|
|
143
|
+
sessionLimit: 'REMOTE_RELAY_SESSION_LIMIT',
|
|
144
|
+
// and an agent tried to prompt, stop, archive, wait for or answer itself.
|
|
145
|
+
ownSession: 'REMOTE_RELAY_OWN_SESSION',
|
|
122
146
|
internal: 'REMOTE_RELAY_INTERNAL',
|
|
123
147
|
// Prefix: the remote answered with this HTTP status, e.g. REMOTE_RELAY_HTTP_409.
|
|
124
148
|
httpPrefix: 'REMOTE_RELAY_HTTP_',
|
|
@@ -131,6 +155,10 @@ export const REMOTE_RELAY_SETTING_KEYS = Object.freeze({
|
|
|
131
155
|
relays: 'remote_relays',
|
|
132
156
|
publicUrl: 'relay_public_url',
|
|
133
157
|
inboundEnabled: 'remote_relay_inbound_enabled',
|
|
158
|
+
// Agent sessions: the local target's switch (off unless set) and the longest
|
|
159
|
+
// wait of one tool call, for local and paired targets alike.
|
|
160
|
+
agentSessionsEnabled: 'agent_sessions_enabled',
|
|
161
|
+
maxWaitSeconds: 'remote_relay_max_wait_seconds',
|
|
134
162
|
});
|
|
135
163
|
|
|
136
164
|
// Headers a relay adds to every call it forwards to a remote. The remote uses
|
|
@@ -142,22 +170,42 @@ export const REMOTE_RELAY_HEADERS = Object.freeze({
|
|
|
142
170
|
});
|
|
143
171
|
|
|
144
172
|
export const REMOTE_RELAY_SOCKET_EVENT = 'remote_relays_updated';
|
|
173
|
+
// Carries the payload of GET /api/settings/agent-sessions.
|
|
174
|
+
export const AGENT_SESSIONS_SOCKET_EVENT = 'agent_sessions_settings_updated';
|
|
175
|
+
|
|
176
|
+
// The local target: the alias that always names the relay the agent runs on,
|
|
177
|
+
// and the id it has wherever a paired relay has its registry id (the
|
|
178
|
+
// remembered approval is stored as an unlock under it).
|
|
179
|
+
export const REMOTE_RELAY_LOCAL_ALIAS = 'this';
|
|
180
|
+
export const REMOTE_RELAY_LOCAL_ID = 'self';
|
|
181
|
+
export const REMOTE_RELAY_CLOUD_PROVIDER = 'claude-cloud';
|
|
145
182
|
|
|
146
183
|
export const REMOTE_RELAY_TOOL_DESCRIPTION =
|
|
147
|
-
'Work with sessions on
|
|
148
|
-
+ '
|
|
149
|
-
+ 'wait for its reply, and answer or stop it.
|
|
150
|
-
+ '
|
|
151
|
-
+ 'to this conversation. Start with {action:"list_relays"}. A relay stays locked until '
|
|
184
|
+
'Work with sessions on OAR relays: the other relays this relay is paired with ("remote relays") '
|
|
185
|
+
+ 'and, when its owner allows it, this relay itself. List the relays, list or read their '
|
|
186
|
+
+ 'sessions, prompt a session, start a new one, wait for its reply, and answer or stop it. '
|
|
187
|
+
+ 'Start with {action:"list_relays"}. A paired relay stays locked until '
|
|
152
188
|
+ 'the user mentions it in this conversation (for example @name) or an agent on that '
|
|
153
189
|
+ 'relay writes to it; from then on it stays open for the whole conversation. A '
|
|
154
190
|
+ 'locked relay refuses everything except list_relays, so ask the user to mention it. '
|
|
191
|
+
+ 'Use a paired relay only when the user asks for work there or mentions it, or to answer '
|
|
192
|
+
+ 'an agent on it that wrote to this conversation. '
|
|
193
|
+
+ 'This relay is listed with self:true and is addressed by its name or as "this"; it needs '
|
|
194
|
+
+ 'no mention. Use it to hand parts of your task to other sessions: create_session up to '
|
|
195
|
+
+ `${REMOTE_RELAY_LIMITS.localActiveSessions} sessions that work at the same time (any provider relay_info lists: local ones `
|
|
196
|
+
+ 'with cwd, provider "claude-cloud" with repo and an optional branch), then wait for and read '
|
|
197
|
+
+ 'each one. The first create_session here shows the user an approval card once per '
|
|
198
|
+
+ 'conversation. A session you created cannot create sessions itself, and every session '
|
|
199
|
+
+ 'runs on the relay owner\'s accounts, so start only what the task needs. A new session '
|
|
200
|
+
+ 'knows nothing of this conversation: write a self-contained prompt, and for '
|
|
201
|
+
+ '"claude-cloud" (a sandbox with a fresh clone of the repository) name the branch to '
|
|
202
|
+
+ 'work on and to push. '
|
|
155
203
|
+ 'send and create_session queue the prompt and wait up to wait_seconds (default 120, '
|
|
156
|
-
+ 'max 600) for the reply. If the
|
|
204
|
+
+ 'max set on the relay, default 600) for the reply. If the turn is still running you get status '
|
|
157
205
|
+ '"running" with progress: call {action:"wait", relay, session, message_id} to keep '
|
|
158
|
-
+ 'waiting.
|
|
206
|
+
+ 'waiting. An agent that answers but keeps working in the background also comes '
|
|
159
207
|
+ 'back as "running", with its latest reply so far; "done" means it has finished. '
|
|
160
|
-
+ 'If a result lists pendingQuestions, the
|
|
208
|
+
+ 'If a result lists pendingQuestions, the other agent is waiting for an '
|
|
161
209
|
+ 'answer: ask the user (unless they told you to decide yourself) and pass the answer '
|
|
162
210
|
+ 'with answer_question. Write actions can first need the user\'s approval. Always '
|
|
163
211
|
+ 'tell the user which relay and session you worked on.';
|
|
@@ -168,21 +216,22 @@ export const REMOTE_RELAY_TOOL_INPUT_SCHEMA = Object.freeze({
|
|
|
168
216
|
action: {
|
|
169
217
|
type: 'string',
|
|
170
218
|
enum: [...REMOTE_RELAY_ACTIONS],
|
|
171
|
-
description: 'list_relays:
|
|
219
|
+
description: 'list_relays: the relays (this one included when allowed) and whether each is unlocked here. relay_info: providers, models, '
|
|
172
220
|
+ 'workspaces of a relay. list_sessions / read_session: discover and read sessions. send: prompt an '
|
|
173
221
|
+ 'existing session. create_session: start a new session with a first prompt. wait: keep waiting for '
|
|
174
222
|
+ 'a reply. answer_question: answer a question the remote agent asked. stop: stop the running turn. '
|
|
175
223
|
+ 'archive: archive a session.',
|
|
176
224
|
},
|
|
177
|
-
relay: { type: 'string', description: 'Relay name (or its host) from list_relays. Required except for list_relays.' },
|
|
178
|
-
session: { type: 'string', description: '
|
|
179
|
-
text: { type: 'string', description: 'send / create_session: the prompt for the
|
|
225
|
+
relay: { type: 'string', description: 'Relay name (or its host) from list_relays, or "this" for the relay you run on. Required except for list_relays.' },
|
|
226
|
+
session: { type: 'string', description: 'Session (conversation) id from list_sessions or create_session.' },
|
|
227
|
+
text: { type: 'string', description: 'send / create_session: the prompt for the other agent.' },
|
|
180
228
|
message_id: { type: 'string', description: 'wait: the message_id that send or create_session returned.' },
|
|
181
229
|
wait_seconds: {
|
|
182
230
|
type: 'integer',
|
|
183
231
|
minimum: 0,
|
|
184
|
-
maximum: REMOTE_RELAY_LIMITS.
|
|
185
|
-
description: 'send / create_session / wait: how long to wait for the reply (default 120, 0 = do not wait
|
|
232
|
+
maximum: REMOTE_RELAY_LIMITS.waitMaxCeilingSeconds,
|
|
233
|
+
description: 'send / create_session / wait: how long to wait for the reply (default 120, 0 = do not wait; '
|
|
234
|
+
+ 'the relay limits it to its own maximum, 600 unless its owner changed it).',
|
|
186
235
|
},
|
|
187
236
|
scope: {
|
|
188
237
|
type: 'string',
|
|
@@ -211,7 +260,16 @@ export const REMOTE_RELAY_TOOL_INPUT_SCHEMA = Object.freeze({
|
|
|
211
260
|
description: 'create_session: model id (default: this session\'s model when the remote offers it, else the remote\'s default). '
|
|
212
261
|
+ 'send: switch the remote session to this model (default: it keeps its own).',
|
|
213
262
|
},
|
|
214
|
-
cwd: { type: 'string', description: 'create_session: working directory on the
|
|
263
|
+
cwd: { type: 'string', description: 'create_session: working directory on the target relay (default: its default workspace). Not for claude-cloud.' },
|
|
264
|
+
repo: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'create_session with provider "claude-cloud" (required there, an error with any other provider): '
|
|
267
|
+
+ 'the GitHub repository the cloud session clones, as https://github.com/owner/repo or owner/repo.',
|
|
268
|
+
},
|
|
269
|
+
branch: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'create_session with provider "claude-cloud": the branch to check out (default: the repository\'s default branch).',
|
|
272
|
+
},
|
|
215
273
|
mode: {
|
|
216
274
|
type: 'string',
|
|
217
275
|
enum: [...REMOTE_RELAY_MODES],
|
|
@@ -257,13 +315,46 @@ function invalid(error) {
|
|
|
257
315
|
|
|
258
316
|
const SESSION_ACTIONS = new Set(['read_session', 'wait', 'send', 'stop', 'archive']);
|
|
259
317
|
|
|
318
|
+
/**
|
|
319
|
+
* The longest wait a relay may be set to allow, from whatever was stored or
|
|
320
|
+
* sent: a whole number of seconds inside the range, or the default.
|
|
321
|
+
*/
|
|
322
|
+
export function normalizeRemoteRelayMaxWaitSeconds(value) {
|
|
323
|
+
const number = Number(value);
|
|
324
|
+
if (value === null || value === undefined || value === '' || !Number.isFinite(number)) {
|
|
325
|
+
return REMOTE_RELAY_LIMITS.waitMaxSeconds;
|
|
326
|
+
}
|
|
327
|
+
return Math.min(
|
|
328
|
+
REMOTE_RELAY_LIMITS.waitMaxCeilingSeconds,
|
|
329
|
+
Math.max(REMOTE_RELAY_LIMITS.waitMaxFloorSeconds, Math.trunc(number)),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The repository of a Claude Cloud session as an agent may write it: any form
|
|
335
|
+
* shared/claude-cloud/repo-url.mjs takes, or the bare `owner/repo`. Returns
|
|
336
|
+
* the canonical https URL, or '' when the text names no GitHub repository.
|
|
337
|
+
*/
|
|
338
|
+
export function normalizeRemoteRelayRepo(value) {
|
|
339
|
+
const text = toText(value);
|
|
340
|
+
if (!text) return '';
|
|
341
|
+
const direct = normalizeGitHubRepoUrl(text);
|
|
342
|
+
if (direct) return direct.repoUrl;
|
|
343
|
+
if (!/^[^/\s:@]+\/[^/\s:@]+$/.test(text)) return '';
|
|
344
|
+
return normalizeGitHubRepoUrl(`https://github.com/${text}`)?.repoUrl || '';
|
|
345
|
+
}
|
|
346
|
+
|
|
260
347
|
/**
|
|
261
348
|
* Validates and normalises one tool call. Adapters call it for a crisp early
|
|
262
349
|
* message; the relay calls it again authoritatively. Returns
|
|
263
350
|
* `{ ok:true, action, args }` with only the fields the action uses, defaults
|
|
264
351
|
* filled in and numbers clamped, or `{ ok:false, code, error }`.
|
|
352
|
+
*
|
|
353
|
+
* `maxWaitSeconds` is the relay's own setting; without it (an adapter, which
|
|
354
|
+
* does not know the setting) wait_seconds is clamped to the ceiling only.
|
|
355
|
+
* `waitLimitedFrom` is set on the result when the relay's setting cut the wait.
|
|
265
356
|
*/
|
|
266
|
-
export function validateRemoteRelayToolInput(input = {}) {
|
|
357
|
+
export function validateRemoteRelayToolInput(input = {}, { maxWaitSeconds } = {}) {
|
|
267
358
|
const action = toText(input?.action).toLowerCase();
|
|
268
359
|
if (!REMOTE_RELAY_ACTIONS.includes(action)) {
|
|
269
360
|
return invalid(`action must be one of: ${REMOTE_RELAY_ACTIONS.join(', ')}`);
|
|
@@ -281,12 +372,20 @@ export function validateRemoteRelayToolInput(input = {}) {
|
|
|
281
372
|
args.session = session;
|
|
282
373
|
}
|
|
283
374
|
|
|
284
|
-
const
|
|
375
|
+
const waitCeiling = clampInteger(input?.wait_seconds, {
|
|
285
376
|
min: 0,
|
|
286
|
-
max: REMOTE_RELAY_LIMITS.
|
|
377
|
+
max: REMOTE_RELAY_LIMITS.waitMaxCeilingSeconds,
|
|
287
378
|
fallback: REMOTE_RELAY_LIMITS.waitDefaultSeconds,
|
|
288
379
|
});
|
|
289
|
-
if (
|
|
380
|
+
if (waitCeiling === null) return invalid('wait_seconds must be a number of seconds');
|
|
381
|
+
const waitMax = maxWaitSeconds === undefined
|
|
382
|
+
? REMOTE_RELAY_LIMITS.waitMaxCeilingSeconds
|
|
383
|
+
: normalizeRemoteRelayMaxWaitSeconds(maxWaitSeconds);
|
|
384
|
+
const waitSeconds = Math.min(waitCeiling, waitMax);
|
|
385
|
+
// What the agent asked for, when the relay's setting allows less.
|
|
386
|
+
const waitLimited = waitSeconds < waitCeiling
|
|
387
|
+
? { waitLimitedFrom: Math.trunc(Math.min(Number(input.wait_seconds), Number.MAX_SAFE_INTEGER)) }
|
|
388
|
+
: {};
|
|
290
389
|
|
|
291
390
|
const optionalEnum = (key, allowed) => {
|
|
292
391
|
const value = toText(input?.[key]).toLowerCase();
|
|
@@ -348,6 +447,29 @@ export function validateRemoteRelayToolInput(input = {}) {
|
|
|
348
447
|
if (!providerCheck.ok) return providerCheck;
|
|
349
448
|
optionalText('cwd', 1000);
|
|
350
449
|
optionalText('title', 200);
|
|
450
|
+
// A Claude Cloud session clones a repository instead of opening a
|
|
451
|
+
// folder: repo and branch belong to that provider alone. A call that
|
|
452
|
+
// names no provider gets this session's, so "repo is missing" for a
|
|
453
|
+
// cloud caller is the relay's to say.
|
|
454
|
+
const repoText = toText(input?.repo);
|
|
455
|
+
const branchText = toText(input?.branch);
|
|
456
|
+
if (args.provider === REMOTE_RELAY_CLOUD_PROVIDER) {
|
|
457
|
+
if (!repoText) {
|
|
458
|
+
return invalid('create_session with provider "claude-cloud" needs repo (https://github.com/owner/repo or owner/repo)');
|
|
459
|
+
}
|
|
460
|
+
if (args.cwd) return invalid('cwd does not apply to provider "claude-cloud": it clones repo instead of opening a folder');
|
|
461
|
+
} else if (repoText || branchText) {
|
|
462
|
+
return invalid('repo and branch go with provider "claude-cloud" only; pass provider "claude-cloud", or cwd for a folder on the relay');
|
|
463
|
+
}
|
|
464
|
+
if (repoText) {
|
|
465
|
+
const repo = normalizeRemoteRelayRepo(repoText);
|
|
466
|
+
if (!repo) return invalid('repo must be a GitHub repository: https://github.com/owner/repo or owner/repo');
|
|
467
|
+
args.repo = repo;
|
|
468
|
+
}
|
|
469
|
+
if (branchText) {
|
|
470
|
+
if (!isValidBranchName(branchText)) return invalid(`branch "${branchText.slice(0, 80)}" is not a valid branch name`);
|
|
471
|
+
args.branch = branchText;
|
|
472
|
+
}
|
|
351
473
|
}
|
|
352
474
|
} else if (action === 'answer_question') {
|
|
353
475
|
const questionId = toText(input?.question_id);
|
|
@@ -361,7 +483,8 @@ export function validateRemoteRelayToolInput(input = {}) {
|
|
|
361
483
|
if (answer) args.answer = answer.slice(0, REMOTE_RELAY_LIMITS.textMax);
|
|
362
484
|
if (choices.length) args.choices = choices;
|
|
363
485
|
}
|
|
364
|
-
|
|
486
|
+
const waits = Object.prototype.hasOwnProperty.call(args, 'wait_seconds');
|
|
487
|
+
return { ok: true, action, args, ...(waits ? waitLimited : {}) };
|
|
365
488
|
}
|
|
366
489
|
|
|
367
490
|
const PERMISSION_RANK = { read: 1, prompt: 2, full: 3 };
|
|
@@ -417,7 +540,12 @@ const ORIGIN_CAPS = {
|
|
|
417
540
|
/**
|
|
418
541
|
* Sanitises an `origin` object received on /api/message or bootstrap (or built
|
|
419
542
|
* locally before forwarding). Returns null when it does not describe an agent
|
|
420
|
-
* on
|
|
543
|
+
* on a relay. Every field is a capped string except `hops` and `local`.
|
|
544
|
+
*
|
|
545
|
+
* `local: true` marks an agent of THIS relay (the local target): the
|
|
546
|
+
* conversation named by `conversationId` lives here. Only the in-process
|
|
547
|
+
* loopback may claim it; the inbound side drops it from anything that came in
|
|
548
|
+
* over HTTP (readRemoteRelayRequest).
|
|
421
549
|
*/
|
|
422
550
|
export function normalizeRemoteRelayOrigin(raw) {
|
|
423
551
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
@@ -430,6 +558,7 @@ export function normalizeRemoteRelayOrigin(raw) {
|
|
|
430
558
|
if (origin.relayUrl && !/^https?:\/\//i.test(origin.relayUrl)) origin.relayUrl = '';
|
|
431
559
|
const hops = Number(raw.hops);
|
|
432
560
|
origin.hops = Number.isInteger(hops) && hops >= 0 ? Math.min(hops, 10) : 1;
|
|
561
|
+
if (raw.local === true) origin.local = true;
|
|
433
562
|
return origin;
|
|
434
563
|
}
|
|
435
564
|
|
|
@@ -443,6 +572,8 @@ function quoteForHeader(value) {
|
|
|
443
572
|
* message carries `origin` (the badge shows the same facts).
|
|
444
573
|
*/
|
|
445
574
|
export function formatRemotePromptHeader(origin) {
|
|
575
|
+
// The same line for an agent of this relay (origin.local): REMOTE_PROMPT_HEADER_PATTERN
|
|
576
|
+
// and every reader that strips it stay as they are.
|
|
446
577
|
const parts = [`[Remote prompt from an agent on relay "${quoteForHeader(origin?.relayName) || 'unknown'}"`];
|
|
447
578
|
const title = quoteForHeader(origin?.conversationTitle);
|
|
448
579
|
if (title) parts.push(`session "${title}"`);
|
|
@@ -41,7 +41,8 @@ export function cloneRemoteRelayToolInputSchema() {
|
|
|
41
41
|
return JSON.parse(JSON.stringify(REMOTE_RELAY_TOOL_INPUT_SCHEMA));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// How many remotes this relay knows
|
|
44
|
+
// How many remotes this relay knows and whether its own sessions are open to
|
|
45
|
+
// agents (`{ count, localEnabled }`): the registration decision.
|
|
45
46
|
export const REMOTE_RELAY_SUMMARY_PATH = '/api/remote-relays/summary';
|
|
46
47
|
// Calls of one conversation the relay is still working on (`{ inflight }`):
|
|
47
48
|
// the Grok inactivity hold, which cannot see the tool call itself.
|
|
@@ -62,9 +63,9 @@ export const REMOTE_RELAY_CANCELLED = 'REMOTE_RELAY_CANCELLED';
|
|
|
62
63
|
export const REMOTE_RELAY_DECISION_TIMEOUT_MS = 5_000;
|
|
63
64
|
|
|
64
65
|
// A hard ceiling for one tool call on the providers that impose their own
|
|
65
|
-
// (MCP clients). The relay already bounds the wait (wait_seconds
|
|
66
|
-
//
|
|
67
|
-
// human, never a
|
|
66
|
+
// (MCP clients). The relay already bounds the wait (wait_seconds up to its
|
|
67
|
+
// "longest wait" setting, at most an hour) but not the time an approval card
|
|
68
|
+
// may sit unanswered, so this only has to outlast a human, never a turn.
|
|
68
69
|
export const REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS = 8 * 60 * 60_000;
|
|
69
70
|
|
|
70
71
|
function toText(value) {
|
|
@@ -146,8 +147,9 @@ export function remoteRelayCancelledResult() {
|
|
|
146
147
|
* contract as executePreviewTool.
|
|
147
148
|
*
|
|
148
149
|
* The call goes out as a long call (`{ longCall: true }`): no client-side
|
|
149
|
-
* timeout at all, because the relay bounds the wait itself (wait_seconds
|
|
150
|
-
* approval time) and fetch's 300 s
|
|
150
|
+
* timeout at all, because the relay bounds the wait itself (wait_seconds,
|
|
151
|
+
* cut to the relay's own maximum, plus approval time) and fetch's 300 s
|
|
152
|
+
* header limit would cut both short.
|
|
151
153
|
* `signal` (the provider's cancellation) is the only way to end it early.
|
|
152
154
|
*/
|
|
153
155
|
export async function executeRemoteRelayTool(input, { api, conversationId = '', signal = null } = {}) {
|
|
@@ -176,7 +178,8 @@ export async function executeRemoteRelayTool(input, { api, conversationId = '',
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
/**
|
|
179
|
-
* Whether a worker should register the tool: at least one remote is paired
|
|
181
|
+
* Whether a worker should register the tool: at least one remote is paired,
|
|
182
|
+
* or this relay lets agents start and use sessions on it (the local target).
|
|
180
183
|
* Fails closed — any error, a malformed answer or a slow relay leaves the tool
|
|
181
184
|
* out, and the next worker start asks again.
|
|
182
185
|
*/
|
|
@@ -188,7 +191,7 @@ export async function shouldRegisterRemoteRelayTool({ api, timeoutMs = REMOTE_RE
|
|
|
188
191
|
timeoutMs,
|
|
189
192
|
'remote relay summary',
|
|
190
193
|
);
|
|
191
|
-
return Number(response?.count) > 0;
|
|
194
|
+
return Number(response?.count) > 0 || response?.localEnabled === true;
|
|
192
195
|
} catch {
|
|
193
196
|
return false;
|
|
194
197
|
}
|
|
@@ -84,6 +84,7 @@ export function steerAgentLabelForProvider(providerType) {
|
|
|
84
84
|
case 'openai': return 'Copilot';
|
|
85
85
|
case 'cursor': return 'Cursor';
|
|
86
86
|
case 'grok': return 'Grok';
|
|
87
|
+
case 'claude-cloud': return 'Claude Cloud';
|
|
87
88
|
default: return DEFAULT_STEER_AGENT_LABEL;
|
|
88
89
|
}
|
|
89
90
|
}
|