@oar-sh/oar 0.9.3 → 0.9.5
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/.github/extensions/web-relay/extension.mjs +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +98 -0
- package/README.md +76 -14
- package/package.json +5 -2
- package/server/README.md +43 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +100 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +385 -43
- package/server/claude-worker/claude-session-worker.mjs +5 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +12 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +83 -9
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +5 -0
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-session-worker.mjs +5 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +8 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-session-worker.mjs +5 -0
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +103 -0
- package/server/public/app/bootstrap.js +21 -6
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-view.js +100 -7
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +4 -1
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +48 -5
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +4 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/steered-turn-merge.mjs +10 -1
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +4 -0
- package/server/public/index.html +264 -11
- package/server/relay-tools.md +5 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/question-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +98 -19
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +84 -31
- package/server/server-runtime.mjs +238 -0
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/remote-relay-caller-context.mjs +346 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1743 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/session-worker-launch-service.mjs +148 -14
- package/shared/remote-relay-contract.mjs +556 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/steer-settle-failure.mjs +6 -0
- package/shared/steer-settle-markers.mjs +16 -0
- package/shared/worker-runtime/api-client.mjs +112 -22
- package/shared/worker-runtime/worker-log-file.mjs +97 -0
|
@@ -6,6 +6,17 @@ import os from 'os';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
OAR_MCP_SERVER_NAME,
|
|
11
|
+
OAR_MCP_SERVER_SCRIPT_PATH,
|
|
12
|
+
OAR_MCP_TOOL_TIMEOUT_MS,
|
|
13
|
+
buildOarMcpServerLaunch,
|
|
14
|
+
isOarMcpServerDisabled,
|
|
15
|
+
} from '../mcp/oar-mcp-server.mjs';
|
|
16
|
+
import { WORKER_LOG_FILE_ENV } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
17
|
+
|
|
18
|
+
export { isOarMcpServerDisabled };
|
|
19
|
+
|
|
9
20
|
function shellQuote(value) {
|
|
10
21
|
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
|
11
22
|
}
|
|
@@ -302,7 +313,7 @@ function resolveNodeWorkerDescriptor(env = {}) {
|
|
|
302
313
|
};
|
|
303
314
|
}
|
|
304
315
|
|
|
305
|
-
function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
|
|
316
|
+
function buildPosixWorkerLaunchCommand(targetSessionId, env = {}, extraCliArgs = []) {
|
|
306
317
|
const nodeWorker = resolveNodeWorkerDescriptor(env);
|
|
307
318
|
if (nodeWorker) {
|
|
308
319
|
const nodeExecutable = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || 'node';
|
|
@@ -312,7 +323,88 @@ function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
|
|
|
312
323
|
|| normalizeText(env?.COPILOT_CLI_EXECUTABLE)
|
|
313
324
|
|| normalizeText(env?.COPILOT_CLI_PATH)
|
|
314
325
|
|| 'copilot';
|
|
315
|
-
|
|
326
|
+
const extra = (Array.isArray(extraCliArgs) ? extraCliArgs : []).map((arg) => ` ${shellQuote(arg)}`).join('');
|
|
327
|
+
return `${shellQuote(cliExecutable)} --allow-all --session-id ${shellQuote(targetSessionId)}${extra}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── The OAR MCP server for the extension engine ─────────────────────────────
|
|
331
|
+
//
|
|
332
|
+
// The Copilot CLI takes extra MCP servers per run through
|
|
333
|
+
// `--additional-mcp-config <json>`, where `<json>` may be `@<file>` (CLI
|
|
334
|
+
// 1.0.83 `--help`: "JSON string or file path (prefix with @)"; the file is
|
|
335
|
+
// read as UTF-8 and merged over ~/.copilot/mcp-config.json for that run
|
|
336
|
+
// only). The config goes to a file rather than inline: the argument then
|
|
337
|
+
// crosses cmd.exe/`start` on Windows and `sh -lc` + `script -c` on the tmux
|
|
338
|
+
// path as one plain path, with no JSON to escape through two shells.
|
|
339
|
+
//
|
|
340
|
+
// Server entry shape (the CLI's own `copilot mcp add` writes the same):
|
|
341
|
+
// `{ type: 'local', command, args, env, tools: ['*'], timeout }`. `timeout` is
|
|
342
|
+
// the per-call limit in ms; without it the CLI cuts a tool call off at 180 s,
|
|
343
|
+
// shorter than one remote_relay wait.
|
|
344
|
+
|
|
345
|
+
export function resolveOarMcpServerScriptPath(env = {}, { pathImpl = path } = {}) {
|
|
346
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
347
|
+
if (repoRoot) return pathImpl.join(repoRoot, 'server', 'mcp', 'oar-mcp-server.mjs');
|
|
348
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
349
|
+
if (serverDir) return pathImpl.join(serverDir, 'mcp', 'oar-mcp-server.mjs');
|
|
350
|
+
return OAR_MCP_SERVER_SCRIPT_PATH;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The `--additional-mcp-config` document for one extension-engine session. */
|
|
354
|
+
export function buildCopilotOarMcpConfig(targetSessionId, env = {}, {
|
|
355
|
+
nodePath = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || process.execPath,
|
|
356
|
+
pathImpl = path,
|
|
357
|
+
} = {}) {
|
|
358
|
+
const launch = buildOarMcpServerLaunch({
|
|
359
|
+
nodePath,
|
|
360
|
+
conversationId: targetSessionId,
|
|
361
|
+
env,
|
|
362
|
+
scriptPath: resolveOarMcpServerScriptPath(env, { pathImpl }),
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
mcpServers: {
|
|
366
|
+
[OAR_MCP_SERVER_NAME]: {
|
|
367
|
+
type: 'local',
|
|
368
|
+
command: launch.command,
|
|
369
|
+
args: launch.args,
|
|
370
|
+
env: launch.env,
|
|
371
|
+
tools: ['*'],
|
|
372
|
+
timeout: OAR_MCP_TOOL_TIMEOUT_MS,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Writes the session's MCP config next to its worker log and returns the file
|
|
380
|
+
* path, or null — best-effort like the log itself: a session that cannot get
|
|
381
|
+
* the file still starts, just without the OAR tools. No secrets go in: the
|
|
382
|
+
* server reads the token from the relay config the file points at.
|
|
383
|
+
*/
|
|
384
|
+
export function prepareOarMcpConfigFile(targetSessionId, launchEnv = {}, {
|
|
385
|
+
fsImpl = fs,
|
|
386
|
+
pathImpl = path,
|
|
387
|
+
nodePath = undefined,
|
|
388
|
+
} = {}) {
|
|
389
|
+
if (isOarMcpServerDisabled(launchEnv)) return null;
|
|
390
|
+
try {
|
|
391
|
+
const config = buildCopilotOarMcpConfig(targetSessionId, launchEnv, {
|
|
392
|
+
pathImpl,
|
|
393
|
+
...(nodePath ? { nodePath } : {}),
|
|
394
|
+
});
|
|
395
|
+
const baseDir = resolveWorkerLogDir(launchEnv, { pathImpl });
|
|
396
|
+
fsImpl.mkdirSync(baseDir, { recursive: true });
|
|
397
|
+
const filePath = pathImpl.join(baseDir, `worker-${safeWorkerFileId(targetSessionId)}.mcp.json`);
|
|
398
|
+
fsImpl.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
399
|
+
return filePath;
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function oarMcpCliArgs(configFilePath) {
|
|
406
|
+
const filePath = normalizeText(configFilePath);
|
|
407
|
+
return filePath ? ['--additional-mcp-config', `@${filePath}`] : [];
|
|
316
408
|
}
|
|
317
409
|
|
|
318
410
|
export function normalizeTmuxSessionName(targetSessionId) {
|
|
@@ -416,22 +508,31 @@ export function createWorkerSecretEnvFile(env = {}, {
|
|
|
416
508
|
* their output discarded (`stdio: 'ignore'` / bare tmux exec), so a crash
|
|
417
509
|
* left zero forensic trail — the failure mode that made the 2026-08-11
|
|
418
510
|
* incident undiagnosable. Best-effort by design: a log problem must never
|
|
419
|
-
* block a worker spawn. Naive rotation: >10 MB rolls to `<file>.1
|
|
511
|
+
* block a worker spawn. Naive rotation: >10 MB rolls to `<file>.1`, checked
|
|
512
|
+
* at each launch on every platform.
|
|
420
513
|
*/
|
|
514
|
+
function resolveWorkerLogDir(launchEnv = {}, { pathImpl = path } = {}) {
|
|
515
|
+
const explicitDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_LOG_DIR);
|
|
516
|
+
const serverDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
517
|
+
// `new URL(...).pathname` yields '/C:/git/...' on Windows, which pathImpl.join
|
|
518
|
+
// then mangles into '\C:\git\...'; fileURLToPath decodes it to a real host path.
|
|
519
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
520
|
+
return explicitDir
|
|
521
|
+
|| (serverDir ? pathImpl.join(serverDir, 'logs') : pathImpl.join(moduleDir, '..', 'logs'));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function safeWorkerFileId(targetSessionId) {
|
|
525
|
+
return String(targetSessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'worker';
|
|
526
|
+
}
|
|
527
|
+
|
|
421
528
|
export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
|
|
422
529
|
fsImpl = fs,
|
|
423
530
|
pathImpl = path,
|
|
424
531
|
} = {}) {
|
|
425
532
|
try {
|
|
426
|
-
const
|
|
427
|
-
const serverDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
428
|
-
// `new URL(...).pathname` yields '/C:/git/...' on Windows, which pathImpl.join
|
|
429
|
-
// then mangles into '\C:\git\...'; fileURLToPath decodes it to a real host path.
|
|
430
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
431
|
-
const baseDir = explicitDir
|
|
432
|
-
|| (serverDir ? pathImpl.join(serverDir, 'logs') : pathImpl.join(moduleDir, '..', 'logs'));
|
|
533
|
+
const baseDir = resolveWorkerLogDir(launchEnv, { pathImpl });
|
|
433
534
|
fsImpl.mkdirSync(baseDir, { recursive: true });
|
|
434
|
-
const safeId =
|
|
535
|
+
const safeId = safeWorkerFileId(targetSessionId);
|
|
435
536
|
const logPath = pathImpl.join(baseDir, `worker-${safeId}.log`);
|
|
436
537
|
try {
|
|
437
538
|
const stats = fsImpl.statSync(logPath);
|
|
@@ -446,6 +547,9 @@ export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
|
|
|
446
547
|
export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
447
548
|
secretEnvFilePath = '',
|
|
448
549
|
workerLogPath = '',
|
|
550
|
+
// Extension engine only: appended to the CLI command, each shell-quoted
|
|
551
|
+
// (and quoted again with it for `script -c`).
|
|
552
|
+
extraCliArgs = [],
|
|
449
553
|
} = {}) {
|
|
450
554
|
const launchEnv = {
|
|
451
555
|
...env,
|
|
@@ -503,6 +607,10 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
|
503
607
|
'GROK_RELAY_MODEL',
|
|
504
608
|
'GROK_CLI_COMMAND',
|
|
505
609
|
'GROK_ALWAYS_APPROVE',
|
|
610
|
+
// The OAR MCP server kill switch (the Grok worker reads it too), and the
|
|
611
|
+
// extension engine's "it is attached" marker.
|
|
612
|
+
'OAR_MCP_SERVER',
|
|
613
|
+
'OAR_MCP_SERVER_ATTACHED',
|
|
506
614
|
'SESSION_ID',
|
|
507
615
|
'COPILOT_WORKSPACE_ROOT',
|
|
508
616
|
'INIT_CWD',
|
|
@@ -515,7 +623,7 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
|
515
623
|
const secretPrefix = normalizedSecretEnvFilePath
|
|
516
624
|
? `. ${shellQuote(normalizedSecretEnvFilePath)} || exit $?; rm -f ${shellQuote(normalizedSecretEnvFilePath)}; rmdir ${shellQuote(path.dirname(normalizedSecretEnvFilePath))}; `
|
|
517
625
|
: '';
|
|
518
|
-
const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv);
|
|
626
|
+
const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv, extraCliArgs);
|
|
519
627
|
if (resolveNodeWorkerDescriptor(launchEnv)) {
|
|
520
628
|
// Node workers (Claude, Cursor, Grok) are plain processes; no pseudo-TTY
|
|
521
629
|
// needed. Their output is teed to the worker log so a crash leaves a
|
|
@@ -574,6 +682,12 @@ export async function launchSessionCli({
|
|
|
574
682
|
detachedPollDelayMs = 200,
|
|
575
683
|
allowProcessReuse = true,
|
|
576
684
|
createSecretEnvFileImpl = createWorkerSecretEnvFile,
|
|
685
|
+
// Extension engine: writes the session's OAR MCP config, returns its path
|
|
686
|
+
// (or null to launch without it).
|
|
687
|
+
prepareOarMcpConfigImpl = prepareOarMcpConfigFile,
|
|
688
|
+
// Windows console launch: resolves (and rotates) the worker log the worker
|
|
689
|
+
// then writes itself; returns its path or null.
|
|
690
|
+
prepareWorkerLogFileImpl = prepareWorkerLogFile,
|
|
577
691
|
} = {}) {
|
|
578
692
|
const target = String(targetSessionId || '').trim();
|
|
579
693
|
if (!target) throw new Error('missing-target-session-id');
|
|
@@ -610,6 +724,17 @@ export async function launchSessionCli({
|
|
|
610
724
|
...launchEnv,
|
|
611
725
|
SESSION_ID: target,
|
|
612
726
|
};
|
|
727
|
+
// Set per launch below, never inherited: a relay started from inside a
|
|
728
|
+
// worker session would otherwise hand its workers that session's log.
|
|
729
|
+
delete launchSessionEnv[WORKER_LOG_FILE_ENV];
|
|
730
|
+
// The extension engine gets the OAR MCP server (remote_relay + preview) the
|
|
731
|
+
// node workers carry in-process; every launch path appends the same args.
|
|
732
|
+
const extensionCliArgs = resolveNodeWorkerDescriptor(launchSessionEnv)
|
|
733
|
+
? []
|
|
734
|
+
: oarMcpCliArgs(prepareOarMcpConfigImpl(target, launchSessionEnv));
|
|
735
|
+
// Tells the in-CLI web-relay extension that `preview` is a real tool in this
|
|
736
|
+
// session, so its prompt context need not teach the HTTP fallback.
|
|
737
|
+
if (extensionCliArgs.length) launchSessionEnv.OAR_MCP_SERVER_ATTACHED = '1';
|
|
613
738
|
|
|
614
739
|
if (isTmuxAvailable({ platform, execFileSyncImpl })) {
|
|
615
740
|
const sessionName = normalizeTmuxSessionName(target);
|
|
@@ -647,6 +772,7 @@ export async function launchSessionCli({
|
|
|
647
772
|
buildTmuxWorkerShellCommand(target, launchSessionEnv, {
|
|
648
773
|
secretEnvFilePath: secretEnvFile?.filePath,
|
|
649
774
|
workerLogPath: tmuxWorkerLogPath || '',
|
|
775
|
+
extraCliArgs: extensionCliArgs,
|
|
650
776
|
}),
|
|
651
777
|
], {
|
|
652
778
|
env: tmuxEnv,
|
|
@@ -717,14 +843,22 @@ export async function launchSessionCli({
|
|
|
717
843
|
'--allow-all',
|
|
718
844
|
'--session-id',
|
|
719
845
|
target,
|
|
846
|
+
...extensionCliArgs,
|
|
720
847
|
])
|
|
721
848
|
: (nodeWorker
|
|
722
849
|
? [nodeWorker.scriptPath, '--session-id', target]
|
|
723
|
-
: ['--allow-all', '--session-id', target]);
|
|
850
|
+
: ['--allow-all', '--session-id', target, ...extensionCliArgs]);
|
|
724
851
|
// POSIX node workers tee stdout/stderr into the worker log (a crash must
|
|
725
852
|
// leave a trail). On win32 the `start` intermediary opens its own console,
|
|
726
|
-
// so fd inheritance cannot reach the worker
|
|
853
|
+
// so fd inheritance cannot reach the worker, and a shell redirect would
|
|
854
|
+
// leave its window empty: the worker is told the file and copies its own
|
|
855
|
+
// output into it (shared/worker-runtime/worker-log-file.mjs). Only the path
|
|
856
|
+
// travels; nothing of the environment or command line is written to the log.
|
|
727
857
|
let detachedStdio = 'ignore';
|
|
858
|
+
if (platform === 'win32' && nodeWorker) {
|
|
859
|
+
const workerLogPath = prepareWorkerLogFileImpl(target, launchSessionEnv);
|
|
860
|
+
if (workerLogPath) launchSessionEnv[WORKER_LOG_FILE_ENV] = workerLogPath;
|
|
861
|
+
}
|
|
728
862
|
if (platform !== 'win32' && nodeWorker) {
|
|
729
863
|
const workerLogPath = prepareWorkerLogFile(target, launchSessionEnv);
|
|
730
864
|
if (workerLogPath) {
|