diff --git a/CHANGELOG.md b/CHANGELOG.md
index f772ed0ef..f48c3a4f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,11 @@ All notable changes to Agent Relay will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased - Patch]
+## [Unreleased - Minor]
+
+### Changed
+
+- `npx -y @agent-relay/connect install` installs a signed standalone probe on a clean Mac without installing the GUI app.
### Fixed
diff --git a/packages/connect/README.md b/packages/connect/README.md
index b101c909f..222d7ead0 100644
--- a/packages/connect/README.md
+++ b/packages/connect/README.md
@@ -7,7 +7,7 @@ or a preinstalled skill:
Run this for me: `npx -y @agent-relay/connect join `
```
-The command reuses a live Agent Relay Desktop probe at version 2026.10.5 or
+The command reuses a live Agent Relay probe at version 2026.10.5 or
newer on macOS, or 2026.10.4 or newer on Linux, when its pointer and socket are
owned by the current user and the pointer is not group- or world-writable.
Unsafe pointer or socket metadata fails closed.
@@ -15,8 +15,9 @@ If an existing socket or Relay process may be restarting, the command retries
liveness for up to 15 seconds. When no eligible probe is available, it
downloads the current release from `AgentWorkforce/relay-desktop-releases`,
verifies the adjacent SHA-256 file, and starts the probe without signing in.
-A responsive old Linux probe instead fails with an update-required error so the
-CLI never starts a second headless probe beside it.
+A responsive old standalone probe fails with an update-required error so the
+CLI never starts a second headless probe beside it. An installed macOS app keeps
+its existing app update path.
If a Linux 2026.10.4 probe cannot identify the calling session during join,
send, status, or leave, the CLI tells the user to update Agent Relay rather than retrying the
same request. A live Linux headless probe cannot be replaced by this CLI while
@@ -26,9 +27,13 @@ it is running; update the running service separately.
`~/.local/lib/agent-relay/current`, a symlink at
`~/.local/bin/agent-relay-probe`, and a detached headless process. No `sudo`
is used.
-- macOS x64 and arm64 use the signed DMG. The app is staged, verified with
- `codesign --verify --deep --strict`, and installed in `/Applications` when
- writable. `~/Applications` is an explicitly untested fallback.
+- macOS x64 and arm64 use a standalone probe tarball notarized by the
+ publisher. The installer verifies its SHA-256 and the extracted binary's
+ pinned Developer ID before swapping it into `~/.local/lib/agent-relay/current` and starting
+ it headless. A clean Mac does not install the GUI app. If an app is already
+ installed but its probe needs updating, the existing signed DMG path updates
+ that app. An app in `/Applications` that this user cannot replace requires
+ an administrator update or moving the app to `~/Applications`.
- For Claude Code, starting the probe sets `"crossSessionInbound": "accept"`
in `~/.claude/settings.json` so replies can be injected into the live
session. The command never signs the user in.
@@ -57,9 +62,10 @@ curl -fsS --unix-socket "$S" -X POST http://relay/setup/direct-delivery \
-H 'content-type: application/json' -d '{"enabled":false}'
```
-On Linux, stop the `agent-relay-probe` process, then remove only
+For a standalone install on Linux or macOS, stop the `agent-relay-probe`
+process, then remove only
`~/.local/bin/agent-relay-probe` and `~/.local/lib/agent-relay/current`. Remove
the pointer under `~/.agentworkforce/desktop/relay-socket` only after the probe
-has stopped. On macOS, quit Agent Relay and move the installed app from
+has stopped. For an installed macOS app, quit Agent Relay and move the app from
`/Applications` (or `~/Applications`) to Trash. The CLI never removes Relay
account data, keys, or other Relay installations.
diff --git a/packages/connect/src/install.js b/packages/connect/src/install.js
index c5c226340..1ebb3d50a 100644
--- a/packages/connect/src/install.js
+++ b/packages/connect/src/install.js
@@ -16,7 +16,7 @@ import {
writeFile,
} from 'node:fs/promises';
import os from 'node:os';
-import { basename, join, relative } from 'node:path';
+import { basename, dirname, join, relative } from 'node:path';
import { requestJson } from './http.js';
const RELEASE = 'https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download';
@@ -54,13 +54,19 @@ export function getPlatformAsset(platform = process.platform, arch = process.arc
throw new InstallError(`Unsupported Linux architecture: ${arch}`, 2);
}
if (platform === 'darwin') {
- if (arch === 'x64') return 'AgentRelay-macOS-x64.dmg';
- if (arch === 'arm64') return 'AgentRelay-macOS-arm64.dmg';
+ if (arch === 'x64') return 'AgentRelay-macOS-x64-probe.tar.gz';
+ if (arch === 'arm64') return 'AgentRelay-macOS-arm64-probe.tar.gz';
throw new InstallError(`Unsupported macOS architecture: ${arch}`, 2);
}
throw new InstallError(`Unsupported platform: ${platform}`, 2);
}
+export function getMacAppAsset(arch = process.arch) {
+ if (arch === 'x64') return 'AgentRelay-macOS-x64.dmg';
+ if (arch === 'arm64') return 'AgentRelay-macOS-arm64.dmg';
+ throw new InstallError(`Unsupported macOS architecture: ${arch}`, 2);
+}
+
export function downloadCommands(platform, tmpDir, asset) {
const destination = join(tmpDir, asset);
const checksum = `${destination}.sha256`;
@@ -92,6 +98,14 @@ export async function verifyMacSignature(staged, run = runCommand) {
}
}
+export async function verifyMacProbeSignature(staged, run = runCommand) {
+ try {
+ await run('codesign', ['--verify', '--strict', `-R=${MAC_SIGNING_REQUIREMENT}`, staged]);
+ } catch {
+ throw new InstallError('Agent Relay probe is not signed by Agent Workforce; refusing installation.');
+ }
+}
+
export async function runCommand(file, args, { cwd, capture = false, allowFailure = false } = {}) {
return new Promise((resolve, reject) => {
const child = spawn(file, args, {
@@ -371,15 +385,17 @@ export async function finishSwap(destination, swap, ready, options = {}) {
}
}
-export async function installLinux({
+async function installHeadless({
home,
+ platform,
arch,
run,
start = startDetachedProbe,
wait = waitForLiveSocket,
+ require = requireCommands,
}) {
- await requireCommands(['curl', 'sha256sum', 'tar']);
- const asset = getPlatformAsset('linux', arch);
+ await require(platform === 'darwin' ? ['codesign', 'curl', 'shasum', 'tar'] : ['curl', 'sha256sum', 'tar']);
+ const asset = getPlatformAsset(platform, arch);
const tmpDir = await mkdtemp(join(os.tmpdir(), 'agent-relay-connect-'));
let child;
let ready = false;
@@ -392,14 +408,23 @@ export async function installLinux({
const installDir = join(installRoot, 'current');
const stagedDir = join(installRoot, 'current.new');
try {
- await downloadAndVerify('linux', tmpDir, asset, run);
+ await downloadAndVerify(platform, tmpDir, asset, run);
await mkdir(installRoot, { recursive: true });
await rm(stagedDir, { recursive: true, force: true });
await mkdir(stagedDir, { recursive: true });
- await run('tar', ['-xzf', join(tmpDir, asset), '-C', stagedDir]);
+ const archive = join(tmpDir, asset);
+ // The Mac archive needs only its signed helper. Extracting a named member
+ // prevents any additional archive entries from writing into this HOME.
+ await run(
+ 'tar',
+ platform === 'darwin'
+ ? ['-xzf', archive, '-C', stagedDir, 'agent_relay/helpers/agent-relay-probe']
+ : ['-xzf', archive, '-C', stagedDir]
+ );
const stagedProbe = await findProbe(stagedDir);
if (!stagedProbe) throw new InstallError(`agent-relay-probe not found or not executable in ${asset}.`);
+ if (platform === 'darwin') await verifyMacProbeSignature(stagedProbe, run);
const probeRelativePath = relative(stagedDir, stagedProbe);
swap = await swapWithBackup(installDir, stagedDir);
const probe = join(installDir, probeRelativePath);
@@ -413,7 +438,10 @@ export async function installLinux({
try {
previousLinkTarget = await readlink(link);
} catch (error) {
- if (error?.code !== 'ENOENT' && error?.code !== 'EINVAL') throw error;
+ if (error?.code === 'EINVAL') {
+ throw new InstallError(`Refusing to replace a non-symlink at ${link}.`);
+ }
+ if (error?.code !== 'ENOENT') throw error;
}
await rm(link, { force: true });
await symlink(probe, link);
@@ -429,7 +457,7 @@ export async function installLinux({
await logHandle.close();
}
- const result = await wait({ home });
+ const result = await wait({ home, minimumVersion: minimumProbeVersion(platform) });
ready = true;
await finishSwap(installDir, swap, true);
return result;
@@ -473,6 +501,14 @@ export async function installLinux({
}
}
+export async function installLinux(options) {
+ return installHeadless({ ...options, platform: 'linux' });
+}
+
+export async function installMacProbe(options) {
+ return installHeadless({ ...options, platform: 'darwin' });
+}
+
async function processRunning(run) {
const result = await run('pgrep', ['-x', 'RelayDesktop'], { allowFailure: true });
return result.code === 0;
@@ -642,11 +678,22 @@ export async function installMac({
arch,
run,
warn,
+ appPath = null,
wait = waitForLiveSocket,
require = requireCommands,
+ accessPath = access,
}) {
+ if (appPath) {
+ try {
+ await accessPath(dirname(appPath), constants.W_OK);
+ } catch {
+ throw new InstallError(
+ `Cannot replace Agent Relay at ${appPath}; ask an administrator to update it or move the app to ~/Applications.`
+ );
+ }
+ }
await require(['codesign', 'curl', 'ditto', 'hdiutil', 'open', 'osascript', 'pgrep', 'shasum']);
- const asset = getPlatformAsset('darwin', arch);
+ const asset = getMacAppAsset(arch);
const tmpDir = await mkdtemp(join(os.tmpdir(), 'agent-relay-connect-'));
let volume = '';
let staged = '';
@@ -667,14 +714,16 @@ export async function installMac({
await quitRelayDesktop(run);
- app = '/Applications/Agent Relay.app';
- try {
- await access('/Applications', constants.W_OK);
- } catch {
- const applications = join(home, 'Applications');
- await mkdir(applications, { recursive: true });
- app = join(applications, 'Agent Relay.app');
- warn(`Using the untested per-user Applications fallback: ${app}`);
+ app = appPath || '/Applications/Agent Relay.app';
+ if (!appPath) {
+ try {
+ await access('/Applications', constants.W_OK);
+ } catch {
+ const applications = join(home, 'Applications');
+ await mkdir(applications, { recursive: true });
+ app = join(applications, 'Agent Relay.app');
+ warn(`Using the untested per-user Applications fallback: ${app}`);
+ }
}
staged = `${app}.new`;
@@ -718,6 +767,30 @@ export async function installMac({
}
}
+export async function findInstalledMacApp(home = os.homedir()) {
+ for (const app of ['/Applications/Agent Relay.app', join(home, 'Applications/Agent Relay.app')]) {
+ try {
+ if ((await stat(app)).isDirectory()) return app;
+ } catch (error) {
+ if (error?.code !== 'ENOENT') {
+ throw new InstallError(`Could not inspect the Agent Relay app: ${error.message}`);
+ }
+ }
+ }
+ return null;
+}
+
+export async function hasStandaloneMacProbe(home = os.homedir()) {
+ const link = join(home, '.local/bin/agent-relay-probe');
+ try {
+ const target = await readlink(link);
+ return target.startsWith(`${join(home, '.local/lib/agent-relay')}/`);
+ } catch (error) {
+ if (error?.code === 'ENOENT') return false;
+ throw new InstallError(`Could not inspect the standalone Agent Relay probe: ${error.message}`);
+ }
+}
+
export async function ensureProbe({
home = os.homedir(),
platform = process.platform,
@@ -731,7 +804,10 @@ export async function ensureProbe({
start = startDetachedProbe,
wait = waitForLiveSocket,
installLinuxFn = installLinux,
+ installMacProbeFn = installMacProbe,
installMacFn = installMac,
+ installedMacApp = findInstalledMacApp,
+ standaloneMacProbe = hasStandaloneMacProbe,
acquire = acquireInstallLock,
} = {}) {
const existing = await findRecoveringProbe({ home, find, run, active, now, sleep });
@@ -747,17 +823,33 @@ export async function ensureProbe({
const installLock = await acquire({ home, platform, now, sleep });
try {
+ const appPath = platform === 'darwin' ? await installedMacApp(home) : null;
const afterLock = await find(home, 1_000);
if (probeSupportedOnPlatform(afterLock, platform)) {
return { ...afterLock, installed: false };
- } else if (afterLock && platform === 'linux') {
- requireSupportedProbe(afterLock, platform);
+ }
+ // A short lookup can miss a core observed immediately before the lock.
+ // Keep that unsupported observation until the guarded install decision.
+ const unsupported = afterLock || existing;
+ if (platform === 'linux') {
+ if (unsupported) requireSupportedProbe(unsupported, platform);
+ } else if (
+ platform === 'darwin' &&
+ unsupported &&
+ !probeSupportedOnPlatform(unsupported, platform) &&
+ (!appPath || (await standaloneMacProbe(home)))
+ ) {
+ // An old standalone core may still own the socket even if the app is
+ // installed. Updating the app would leave that core running beside it.
+ requireSupportedProbe(unsupported, platform);
}
const result =
platform === 'linux'
? await installLinuxFn({ home, arch, run, start, wait })
- : await installMacFn({ home, arch, run, warn, wait });
+ : appPath
+ ? await installMacFn({ home, arch, run, warn, wait, appPath })
+ : await installMacProbeFn({ home, arch, run, start, wait });
return { ...result, installed: true };
} finally {
await installLock.release();
diff --git a/packages/connect/tests/connect.test.ts b/packages/connect/tests/connect.test.ts
index ec779275c..3b9dffc70 100644
--- a/packages/connect/tests/connect.test.ts
+++ b/packages/connect/tests/connect.test.ts
@@ -24,8 +24,11 @@ import {
finishSwap,
findLiveSocket,
findRecoveringProbe,
+ getMacAppAsset,
getPlatformAsset,
+ hasStandaloneMacProbe,
installMac,
+ installMacProbe,
installLinux,
linuxProbeSessionUpdateHint,
macStageCommands,
@@ -103,7 +106,7 @@ async function runCli(home: string, args: string[], input = '') {
}
describe('@agent-relay/connect CLI', () => {
- it('joins through a fake socket, keeps the host claim private, and makes no blocking follow-up send', async () => {
+ it('joins through a fake socket, keeps the host claim private, and is repeatable without a duplicate hello', async () => {
const joins: Array> = [];
const hellos: string[] = [];
const { home } = await listen((request, response, body) => {
@@ -124,9 +127,9 @@ describe('@agent-relay/connect CLI', () => {
participants: [],
},
});
- } else if (request.url?.startsWith('/connect/send')) {
+ } else if (request.url === '/connect/send?to=host-agent') {
hellos.push(body);
- json(response, { ok: false, error: { code: 'unexpected_send' } }, 500);
+ json(response, { ok: true, data: { sent: [{ to: 'host-agent', message_id: 'message-1' }] } });
} else {
json(response, { ok: false, error: { code: 'unexpected', message: request.url } }, 404);
}
@@ -218,40 +221,6 @@ describe('@agent-relay/connect CLI', () => {
expect(sendRequests).toBe(0);
});
- it('maps retry-safe join timeout and pending-operation errors', async () => {
- const { home } = await listen((request, response, body) => {
- if (request.url === '/setup/status') {
- json(response, { ok: true, data: { version: '2026.10.5' } });
- } else if (request.url === '/connect/join') {
- const pending = JSON.parse(body).link === 'connect-pending';
- json(
- response,
- {
- ok: false,
- error: {
- code: pending ? 'connect_join_pending' : 'connect_join_timeout',
- message: 'safe to retry',
- },
- },
- pending ? 409 : 504
- );
- }
- });
-
- const joined = await runCli(home, ['join', 'connect-timed-out']);
- expect(joined).toEqual({
- code: 8,
- stdout: '',
- stderr: 'Relay Connect join timed out; retry the same join safely.\n',
- });
- const pending = await runCli(home, ['join', 'connect-pending']);
- expect(pending).toEqual({
- code: 8,
- stdout: '',
- stderr: 'A previous join has an unknown outcome; retry with the same link and options.\n',
- });
- });
-
it('maps socket errors and preserves their code in JSON mode', async () => {
const { home } = await listen((request, response) => {
if (request.url === '/setup/status') {
@@ -278,8 +247,9 @@ describe('probe installer', () => {
it('selects release assets for every supported platform and architecture', () => {
expect(getPlatformAsset('linux', 'x64')).toBe('AgentRelay-Linux-x64.tar.gz');
expect(getPlatformAsset('linux', 'arm64')).toBe('AgentRelay-Linux-arm64.tar.gz');
- expect(getPlatformAsset('darwin', 'x64')).toBe('AgentRelay-macOS-x64.dmg');
- expect(getPlatformAsset('darwin', 'arm64')).toBe('AgentRelay-macOS-arm64.dmg');
+ expect(getPlatformAsset('darwin', 'x64')).toBe('AgentRelay-macOS-x64-probe.tar.gz');
+ expect(getPlatformAsset('darwin', 'arm64')).toBe('AgentRelay-macOS-arm64-probe.tar.gz');
+ expect(getMacAppAsset('arm64')).toBe('AgentRelay-macOS-arm64.dmg');
expect(() => getPlatformAsset('linux', 'riscv64')).toThrow('Unsupported Linux architecture');
expect(() => getPlatformAsset('win32', 'x64')).toThrow('Unsupported platform');
});
@@ -335,6 +305,7 @@ describe('probe installer', () => {
const result = await ensureProbe({
home: '/tmp/old-mac-probe',
platform: 'darwin',
+ installedMacApp: async () => '/Applications/Agent Relay.app',
find: async () => ({
socketPath: '/tmp/old.sock',
status: { ok: true, data: { version: '2026.10.4' } },
@@ -354,6 +325,119 @@ describe('probe installer', () => {
expect(result).toMatchObject({ socketPath: '/tmp/new.sock', installed: true });
});
+ it('chooses a standalone probe for a clean Mac and the app updater for an installed app', async () => {
+ for (const app of [null, '/Applications/Agent Relay.app']) {
+ const calls: string[] = [];
+ const result = await ensureProbe({
+ home: '/tmp/clean-mac',
+ platform: 'darwin',
+ find: async () => null,
+ active: async () => false,
+ installedMacApp: async () => app,
+ acquire: async () => ({ release: async () => {} }),
+ installMacProbeFn: async () => {
+ calls.push('probe');
+ return { socketPath: '/tmp/probe.sock', status: { ok: true } };
+ },
+ installMacFn: async () => {
+ calls.push('app');
+ return { socketPath: '/tmp/app.sock', status: { ok: true } };
+ },
+ });
+ expect(calls).toEqual([app ? 'app' : 'probe']);
+ expect(result.installed).toBe(true);
+ }
+ });
+
+ it('refuses to start a second standalone core beside an old live Mac probe', async () => {
+ await expect(
+ ensureProbe({
+ home: '/tmp/old-standalone-mac',
+ platform: 'darwin',
+ find: async () => ({ version: '2026.10.3', supported: false }),
+ installedMacApp: async () => null,
+ installMacProbeFn: async () => {
+ throw new Error('must not install');
+ },
+ })
+ ).rejects.toMatchObject({ code: 'probe_too_old' });
+ });
+
+ it('refuses an old standalone core even when the GUI app is installed', async () => {
+ let appLookups = 0;
+ await expect(
+ ensureProbe({
+ home: '/tmp/old-standalone-with-app',
+ platform: 'darwin',
+ find: async () => ({ version: '2026.10.3', supported: false }),
+ installedMacApp: async () => {
+ appLookups += 1;
+ return '/Applications/Agent Relay.app';
+ },
+ standaloneMacProbe: async () => true,
+ acquire: async () => ({ release: async () => {} }),
+ installMacFn: async () => {
+ throw new Error('must not install');
+ },
+ })
+ ).rejects.toMatchObject({ code: 'probe_too_old' });
+ expect(appLookups).toBe(1);
+ });
+
+ it('retains an unsupported probe observed before a short post-lock miss', async () => {
+ let looks = 0;
+ await expect(
+ ensureProbe({
+ home: '/tmp/old-standalone-short-miss',
+ platform: 'darwin',
+ find: async () => (++looks === 1 ? { version: '2026.10.3', supported: false } : null),
+ installedMacApp: async () => null,
+ acquire: async () => ({ release: async () => {} }),
+ installMacProbeFn: async () => {
+ throw new Error('must not install');
+ },
+ })
+ ).rejects.toMatchObject({ code: 'probe_too_old' });
+ expect(looks).toBe(2);
+ });
+
+ it('recognizes the standalone symlink made by this installer', async () => {
+ const home = await mkdtemp(join(os.tmpdir(), 'connect-standalone-link-test-'));
+ cleanups.push(async () => rm(home, { recursive: true, force: true }));
+ expect(await hasStandaloneMacProbe(home)).toBe(false);
+ await mkdir(join(home, '.local/bin'), { recursive: true });
+ await symlink(
+ join(home, '.local/lib/agent-relay/current/agent_relay/helpers/agent-relay-probe'),
+ join(home, '.local/bin/agent-relay-probe')
+ );
+ expect(await hasStandaloneMacProbe(home)).toBe(true);
+ await rm(join(home, '.local/bin/agent-relay-probe'));
+ await writeFile(join(home, '.local/bin/agent-relay-probe'), 'copied binary');
+ await expect(hasStandaloneMacProbe(home)).rejects.toThrow(
+ 'Could not inspect the standalone Agent Relay probe'
+ );
+ });
+
+ it('explains when the installed system app cannot be replaced', async () => {
+ await expect(
+ installMac({
+ home: '/tmp/non-admin-mac',
+ arch: 'arm64',
+ appPath: '/Applications/Agent Relay.app',
+ accessPath: async () => {
+ throw new Error('permission denied');
+ },
+ require: async () => {
+ throw new Error('must not download');
+ },
+ run: async () => {
+ throw new Error('must not run');
+ },
+ warn: () => {},
+ })
+ ).rejects.toThrow('ask an administrator to update it or move the app to ~/Applications');
+ });
+
it('accepts a live Linux 2026.10.4 probe without starting another one', async () => {
let installs = 0;
const result = await ensureProbe({
@@ -514,6 +598,86 @@ describe('probe installer', () => {
).rejects.toThrow('Agent Relay download is not signed by Agent Workforce; refusing installation.');
});
+ it('verifies the extracted Mac helper against the pinned Developer ID before it can start', async () => {
+ const root = await mkdtemp(join(os.tmpdir(), 'connect-mac-probe-test-'));
+ cleanups.push(async () => rm(root, { recursive: true, force: true }));
+ const home = join(root, 'home');
+ const commands: Array<[string, string[]]> = [];
+ let minimumVersion: string | undefined;
+ const run = async (file: string, args: string[]) => {
+ commands.push([file, args]);
+ if (file === 'tar') {
+ const destination = args[args.indexOf('-C') + 1];
+ const probe = join(destination, 'agent_relay/helpers/agent-relay-probe');
+ await mkdir(join(probe, '..'), { recursive: true });
+ await writeFile(probe, '#!/bin/sh\n');
+ await chmod(probe, 0o755);
+ }
+ return { code: 0, stdout: '', stderr: '' };
+ };
+ const result = await installMacProbe({
+ home,
+ arch: 'arm64',
+ run,
+ require: async () => {},
+ start: async () => ({ pid: undefined, unref() {} }),
+ wait: async (options: { minimumVersion?: string }) => {
+ minimumVersion = options.minimumVersion;
+ return { socketPath: '/tmp/mac-probe.sock', status: { ok: true } };
+ },
+ });
+ expect(result.socketPath).toBe('/tmp/mac-probe.sock');
+ expect(minimumVersion).toBe('2026.10.5');
+ expect(commands.map(([file]) => file)).toEqual(['curl', 'curl', 'shasum', 'tar', 'codesign']);
+ expect(commands[0][1].at(-1)).toContain('AgentRelay-macOS-arm64-probe.tar.gz');
+ expect(commands[3][1].at(-1)).toBe('agent_relay/helpers/agent-relay-probe');
+ expect(commands[4][1].slice(0, 3)).toEqual([
+ '--verify',
+ '--strict',
+ '-R=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "QUJ7SA6X8X"',
+ ]);
+ await expect(
+ readFile(join(home, '.local/lib/agent-relay/current/agent_relay/helpers/agent-relay-probe'), 'utf8')
+ ).resolves.toBe('#!/bin/sh\n');
+ });
+
+ it('rejects an unsigned extracted Mac helper before swapping or starting it', async () => {
+ const root = await mkdtemp(join(os.tmpdir(), 'connect-unsigned-mac-probe-test-'));
+ cleanups.push(async () => rm(root, { recursive: true, force: true }));
+ const home = join(root, 'home');
+ let started = false;
+ const run = async (file: string, args: string[]) => {
+ if (file === 'tar') {
+ const destination = args[args.indexOf('-C') + 1];
+ const probe = join(destination, 'agent_relay/helpers/agent-relay-probe');
+ await mkdir(join(probe, '..'), { recursive: true });
+ await writeFile(probe, '#!/bin/sh\n');
+ await chmod(probe, 0o755);
+ }
+ if (file === 'codesign') throw new Error('signature mismatch');
+ return { code: 0, stdout: '', stderr: '' };
+ };
+ await expect(
+ installMacProbe({
+ home,
+ arch: 'arm64',
+ run,
+ require: async () => {},
+ start: async () => {
+ started = true;
+ throw new Error('must not start');
+ },
+ wait: async () => {
+ throw new Error('must not wait');
+ },
+ })
+ ).rejects.toThrow('Agent Relay probe is not signed by Agent Workforce');
+ expect(started).toBe(false);
+ await expect(
+ readFile(join(home, '.local/lib/agent-relay/current/agent_relay/helpers/agent-relay-probe'))
+ ).rejects.toMatchObject({ code: 'ENOENT' });
+ });
+
it('restores the previous macOS app when the verified replacement cannot be installed', async () => {
const app = '/Applications/Agent Relay.app';
const staged = `${app}.new`;
@@ -598,6 +762,8 @@ describe('probe installer', () => {
arch: 'arm64',
run,
warn: () => {},
+ appPath: app,
+ accessPath: async () => {},
require: async () => {},
wait: async () => {
throw new Error('replacement never became ready');
diff --git a/vitest.connect.config.mjs b/vitest.connect.config.mjs
new file mode 100644
index 000000000..41f48e2a1
--- /dev/null
+++ b/vitest.connect.config.mjs
@@ -0,0 +1 @@
+export default { test: { include: ['packages/connect/tests/**/*.test.ts'] } };