aegis-bridge 2.15.4 → 2.15.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/dist/auth.js +2 -0
- package/dist/channels/telegram.js +13 -2
- package/dist/cli.js +5 -5
- package/dist/file-utils.d.ts +2 -0
- package/dist/file-utils.js +37 -0
- package/dist/hook-settings.js +3 -0
- package/dist/mcp-server.js +16 -2
- package/dist/process-utils.d.ts +4 -0
- package/dist/process-utils.js +73 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/startup.d.ts +2 -2
- package/dist/startup.js +13 -16
- package/dist/swarm-monitor.d.ts +3 -0
- package/dist/swarm-monitor.js +35 -0
- package/dist/tmux.js +3 -0
- package/package.json +2 -1
package/dist/auth.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
|
10
10
|
import { authStoreSchema } from './validation.js';
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { dirname } from 'node:path';
|
|
13
|
+
import { secureFilePermissions } from './file-utils.js';
|
|
13
14
|
/** Default SSE token lifetime: 60 seconds. */
|
|
14
15
|
const SSE_TOKEN_TTL_MS = 60_000;
|
|
15
16
|
/** Max SSE tokens per bearer token to prevent abuse. */
|
|
@@ -61,6 +62,7 @@ export class AuthManager {
|
|
|
61
62
|
await mkdir(dir, { recursive: true });
|
|
62
63
|
}
|
|
63
64
|
await writeFile(this.keysFile, JSON.stringify(this.store, null, 2), { mode: 0o600 });
|
|
65
|
+
await secureFilePermissions(this.keysFile);
|
|
64
66
|
}
|
|
65
67
|
/** Create a new API key. Returns the plaintext key (only shown once). */
|
|
66
68
|
async createKey(name, rateLimit = 100) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Formatting: HTML parse_mode with structured, clean messages.
|
|
8
8
|
*/
|
|
9
|
+
import { homedir } from 'node:os';
|
|
9
10
|
import { esc, bold, code, italic, quickUpdate, taskComplete, alert as styleAlert, } from './telegram-style.js';
|
|
10
11
|
/** Call Telegram Bot API with retry on 429. */
|
|
11
12
|
// ── HTML Helpers ────────────────────────────────────────────────────────────
|
|
@@ -25,12 +26,22 @@ function elapsed(ms) {
|
|
|
25
26
|
return `${h}h ${m % 60}m`;
|
|
26
27
|
}
|
|
27
28
|
function shortPath(path) {
|
|
29
|
+
const normalized = path.replace(/\\/g, '/');
|
|
28
30
|
// Keep only filename or last 2 segments
|
|
29
|
-
const parts =
|
|
31
|
+
const parts = normalized.replace(/^\//, '').split('/');
|
|
30
32
|
if (parts.length <= 2)
|
|
31
33
|
return parts.join('/');
|
|
32
34
|
return '…/' + parts.slice(-2).join('/');
|
|
33
35
|
}
|
|
36
|
+
function shortenHomePath(workDir) {
|
|
37
|
+
const normalized = workDir.replace(/\\/g, '/');
|
|
38
|
+
const home = homedir().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
39
|
+
if (normalized === home)
|
|
40
|
+
return '~';
|
|
41
|
+
if (normalized.startsWith(`${home}/`))
|
|
42
|
+
return `~${normalized.slice(home.length)}`;
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
34
45
|
/**
|
|
35
46
|
* Strip Claude Code internal XML tags from assistant messages.
|
|
36
47
|
* These tags (local-command-*, antml:*, etc.) are CC's internal markup
|
|
@@ -244,7 +255,7 @@ function md2html(md) {
|
|
|
244
255
|
// ── Message Formatting ──────────────────────────────────────────────────────
|
|
245
256
|
function formatSessionCreated(name, workDir, id, meta) {
|
|
246
257
|
const shortId = id.slice(0, 8);
|
|
247
|
-
const shortDir = workDir
|
|
258
|
+
const shortDir = shortenHomePath(workDir);
|
|
248
259
|
const parts = [`${bold(name)} ${code(shortDir)} ${code(shortId)}`];
|
|
249
260
|
const flags = [];
|
|
250
261
|
if (meta?.permissionMode && meta.permissionMode !== 'default')
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `npx aegis-bridge` or `aegis-bridge` starts the server with sensible defaults.
|
|
6
6
|
* Auto-detects tmux and claude CLI, prints helpful startup message.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
@@ -15,9 +15,9 @@ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
|
|
15
15
|
/** Current aegis-bridge version read from package.json at startup. */
|
|
16
16
|
const VERSION = pkg.version;
|
|
17
17
|
/** Check whether a required external dependency can be executed. */
|
|
18
|
-
function checkDependency(
|
|
18
|
+
function checkDependency(command, args) {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
execFileSync(command, args, { stdio: 'ignore', timeout: 5000 });
|
|
21
21
|
return true;
|
|
22
22
|
}
|
|
23
23
|
catch { /* command not found or exited non-zero */
|
|
@@ -188,8 +188,8 @@ async function main() {
|
|
|
188
188
|
process.env.AEGIS_PORT = args[portIdx + 1];
|
|
189
189
|
}
|
|
190
190
|
// Check dependencies
|
|
191
|
-
const hasTmux = checkDependency('tmux', '
|
|
192
|
-
const hasClaude = checkDependency('claude', '
|
|
191
|
+
const hasTmux = checkDependency('tmux', ['-V']);
|
|
192
|
+
const hasClaude = checkDependency('claude', ['--version']);
|
|
193
193
|
if (!hasTmux) {
|
|
194
194
|
console.error(`
|
|
195
195
|
❌ tmux not found.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { chmod } from 'node:fs/promises';
|
|
3
|
+
const PERMISSIONS_TIMEOUT_MS = 5_000;
|
|
4
|
+
export function buildWindowsIcaclsArgs(filePath, account) {
|
|
5
|
+
return [filePath, '/inheritance:r', '/grant:r', `${account}:(R,W)`];
|
|
6
|
+
}
|
|
7
|
+
function runIcacls(filePath, account) {
|
|
8
|
+
const args = buildWindowsIcaclsArgs(filePath, account);
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
execFile('icacls', args, { timeout: PERMISSIONS_TIMEOUT_MS }, (error) => {
|
|
11
|
+
if (error) {
|
|
12
|
+
reject(error);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
resolve();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function secureFilePermissions(filePath, platform = process.platform) {
|
|
20
|
+
if (platform !== 'win32') {
|
|
21
|
+
await chmod(filePath, 0o600);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const username = process.env.USERNAME;
|
|
25
|
+
if (!username) {
|
|
26
|
+
console.warn(`Windows permission hardening skipped for ${filePath}: USERNAME is not set`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const account = process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${username}` : username;
|
|
30
|
+
try {
|
|
31
|
+
await runIcacls(filePath, account);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
35
|
+
console.warn(`Windows permission hardening failed for ${filePath}: ${detail}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/hook-settings.js
CHANGED
|
@@ -19,6 +19,7 @@ import { join, resolve } from 'node:path';
|
|
|
19
19
|
import { tmpdir } from 'node:os';
|
|
20
20
|
import { randomBytes } from 'node:crypto';
|
|
21
21
|
import { ccSettingsSchema, containsTraversalSegment } from './validation.js';
|
|
22
|
+
import { secureFilePermissions } from './file-utils.js';
|
|
22
23
|
function isRecord(value) {
|
|
23
24
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
25
|
}
|
|
@@ -192,6 +193,7 @@ export async function writeHookSettingsFile(baseUrl, sessionId, hookSecret, work
|
|
|
192
193
|
await mkdir(settingsDir, { recursive: true, mode: 0o700 });
|
|
193
194
|
const filePath = join(settingsDir, `hooks-${sessionId}.json`);
|
|
194
195
|
await writeFile(filePath, JSON.stringify(combined, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
196
|
+
await secureFilePermissions(filePath);
|
|
195
197
|
return filePath;
|
|
196
198
|
}
|
|
197
199
|
/**
|
|
@@ -260,6 +262,7 @@ export async function cleanupStaleSessionHooks(workDir, activeSessionIds) {
|
|
|
260
262
|
}
|
|
261
263
|
if (changed) {
|
|
262
264
|
await writeFile(projectSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
265
|
+
await secureFilePermissions(projectSettingsPath);
|
|
263
266
|
}
|
|
264
267
|
}
|
|
265
268
|
catch {
|
package/dist/mcp-server.js
CHANGED
|
@@ -15,13 +15,27 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
15
15
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
|
-
import { dirname, join } from 'node:path';
|
|
18
|
+
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { isValidUUID } from './validation.js';
|
|
21
21
|
// Read version from package.json at startup (matches cli.ts pattern)
|
|
22
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
24
24
|
const VERSION = pkg.version;
|
|
25
|
+
function normalizeWorkDirForCompare(workDir) {
|
|
26
|
+
const isWindowsLikePath = /^[a-zA-Z]:[\\/]/.test(workDir) || workDir.startsWith('\\\\');
|
|
27
|
+
const normalizedPath = (isWindowsLikePath ? workDir : resolve(workDir))
|
|
28
|
+
.replace(/\\/g, '/')
|
|
29
|
+
.replace(/\/+$/, '');
|
|
30
|
+
return process.platform === 'win32' || isWindowsLikePath
|
|
31
|
+
? normalizedPath.toLowerCase()
|
|
32
|
+
: normalizedPath;
|
|
33
|
+
}
|
|
34
|
+
function isSameOrChildWorkDir(candidate, parent) {
|
|
35
|
+
const normalizedCandidate = normalizeWorkDirForCompare(candidate);
|
|
36
|
+
const normalizedParent = normalizeWorkDirForCompare(parent);
|
|
37
|
+
return normalizedCandidate === normalizedParent || normalizedCandidate.startsWith(`${normalizedParent}/`);
|
|
38
|
+
}
|
|
25
39
|
// ── Aegis REST client ───────────────────────────────────────────────
|
|
26
40
|
export class AegisClient {
|
|
27
41
|
baseUrl;
|
|
@@ -65,7 +79,7 @@ export class AegisClient {
|
|
|
65
79
|
sessions = sessions.filter((s) => s.status === filter.status);
|
|
66
80
|
}
|
|
67
81
|
if (filter?.workDir) {
|
|
68
|
-
sessions = sessions.filter((s) => s.workDir
|
|
82
|
+
sessions = sessions.filter((s) => isSameOrChildWorkDir(s.workDir, filter.workDir));
|
|
69
83
|
}
|
|
70
84
|
return sessions;
|
|
71
85
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function buildWindowsFindPidOnPortScript(port: number): string;
|
|
2
|
+
export declare function buildWindowsReadParentPidScript(pid: number): string;
|
|
3
|
+
export declare function findPidOnPort(port: number): Promise<number[]>;
|
|
4
|
+
export declare function readParentPid(pid: number): Promise<number | null>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
const SUBPROCESS_TIMEOUT_MS = 5_000;
|
|
4
|
+
function runCommand(command, args) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
execFile(command, args, { encoding: 'utf-8', timeout: SUBPROCESS_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, (error, stdout) => {
|
|
7
|
+
if (error) {
|
|
8
|
+
reject(error);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
resolve(stdout);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function parsePidLines(output) {
|
|
16
|
+
return [...new Set(output
|
|
17
|
+
.trim()
|
|
18
|
+
.split(/\r?\n/)
|
|
19
|
+
.map(line => parseInt(line.trim(), 10))
|
|
20
|
+
.filter(pid => Number.isInteger(pid) && pid > 0))];
|
|
21
|
+
}
|
|
22
|
+
export function buildWindowsFindPidOnPortScript(port) {
|
|
23
|
+
return [
|
|
24
|
+
`Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue`,
|
|
25
|
+
'Select-Object -ExpandProperty OwningProcess -Unique',
|
|
26
|
+
].join(' | ');
|
|
27
|
+
}
|
|
28
|
+
export function buildWindowsReadParentPidScript(pid) {
|
|
29
|
+
return [
|
|
30
|
+
`Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" -ErrorAction SilentlyContinue`,
|
|
31
|
+
'Select-Object -ExpandProperty ParentProcessId',
|
|
32
|
+
].join(' | ');
|
|
33
|
+
}
|
|
34
|
+
export async function findPidOnPort(port) {
|
|
35
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
const script = buildWindowsFindPidOnPortScript(port);
|
|
40
|
+
const stdout = await runCommand('powershell', ['-NoProfile', '-Command', script]);
|
|
41
|
+
return parsePidLines(stdout);
|
|
42
|
+
}
|
|
43
|
+
const stdout = await runCommand('lsof', ['-ti', `tcp:${port}`]);
|
|
44
|
+
return parsePidLines(stdout);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function readParentPid(pid) {
|
|
51
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
const script = buildWindowsReadParentPidScript(pid);
|
|
56
|
+
const stdout = await runCommand('powershell', ['-NoProfile', '-Command', script]);
|
|
57
|
+
const parent = parseInt(stdout.trim(), 10);
|
|
58
|
+
return Number.isInteger(parent) && parent > 0 ? parent : null;
|
|
59
|
+
}
|
|
60
|
+
if (process.platform !== 'linux') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const status = await readFile(`/proc/${pid}/status`, 'utf-8');
|
|
64
|
+
const match = status.match(/^PPid:\s+(\d+)/m);
|
|
65
|
+
if (!match)
|
|
66
|
+
return null;
|
|
67
|
+
const parent = parseInt(match[1], 10);
|
|
68
|
+
return Number.isInteger(parent) && parent > 0 ? parent : null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1677,7 +1677,7 @@ function registerChannels(cfg) {
|
|
|
1677
1677
|
}
|
|
1678
1678
|
}
|
|
1679
1679
|
// Preserve public export used by tests and external imports.
|
|
1680
|
-
export { readPpid } from './
|
|
1680
|
+
export { readParentPid as readPpid } from './process-utils.js';
|
|
1681
1681
|
async function main() {
|
|
1682
1682
|
// Load configuration
|
|
1683
1683
|
config = await loadConfig();
|
package/dist/startup.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
2
|
export declare function writePidFile(stateDir: string): string;
|
|
3
3
|
export declare function removePidFile(pidFilePath: string): void;
|
|
4
|
-
/** Read
|
|
5
|
-
export declare function readPpid(pid: number): number
|
|
4
|
+
/** Read parent PID with cross-platform fallback. */
|
|
5
|
+
export declare function readPpid(pid: number): Promise<number>;
|
|
6
6
|
export declare function listenWithRetry(app: ReturnType<typeof Fastify>, port: number, host: string, stateDir: string, maxRetries?: number): Promise<void>;
|
package/dist/startup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { findPidOnPort, readParentPid } from './process-utils.js';
|
|
5
5
|
export function writePidFile(stateDir) {
|
|
6
6
|
try {
|
|
7
7
|
const pidFilePath = path.join(stateDir, 'aegis.pid');
|
|
@@ -41,22 +41,22 @@ function pidExists(pid) {
|
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
/** Read
|
|
45
|
-
export function readPpid(pid) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
44
|
+
/** Read parent PID with cross-platform fallback. */
|
|
45
|
+
export async function readPpid(pid) {
|
|
46
|
+
const parent = await readParentPid(pid);
|
|
47
|
+
if (parent === null) {
|
|
48
|
+
throw new Error(`no parent PID available for process ${pid}`);
|
|
49
|
+
}
|
|
50
|
+
return parent;
|
|
51
51
|
}
|
|
52
|
-
function isAncestorPid(pid) {
|
|
52
|
+
async function isAncestorPid(pid) {
|
|
53
53
|
try {
|
|
54
54
|
let current = process.ppid;
|
|
55
55
|
for (let depth = 0; depth < 10 && current > 1; depth++) {
|
|
56
56
|
if (current === pid)
|
|
57
57
|
return true;
|
|
58
58
|
try {
|
|
59
|
-
current = readPpid(current);
|
|
59
|
+
current = await readPpid(current);
|
|
60
60
|
}
|
|
61
61
|
catch {
|
|
62
62
|
break;
|
|
@@ -94,17 +94,14 @@ async function waitForPortRelease(port, maxWaitMs = 5000) {
|
|
|
94
94
|
async function killStalePortHolder(port, stateDir) {
|
|
95
95
|
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 400));
|
|
96
96
|
try {
|
|
97
|
-
const
|
|
98
|
-
if (!output)
|
|
99
|
-
return false;
|
|
100
|
-
const pids = output.split('\n').map(s => parseInt(s.trim(), 10)).filter(n => !Number.isNaN(n));
|
|
97
|
+
const pids = await findPidOnPort(port);
|
|
101
98
|
if (pids.length === 0)
|
|
102
99
|
return false;
|
|
103
100
|
let killed = false;
|
|
104
101
|
for (const pid of pids) {
|
|
105
102
|
if (pid === process.pid)
|
|
106
103
|
continue;
|
|
107
|
-
if (isAncestorPid(pid)) {
|
|
104
|
+
if (await isAncestorPid(pid)) {
|
|
108
105
|
console.warn(`EADDRINUSE recovery: skipping ancestor PID ${pid} on port ${port}`);
|
|
109
106
|
continue;
|
|
110
107
|
}
|
package/dist/swarm-monitor.d.ts
CHANGED
|
@@ -75,10 +75,13 @@ export declare class SwarmMonitor {
|
|
|
75
75
|
private lastResult;
|
|
76
76
|
private timer;
|
|
77
77
|
private eventHandlers;
|
|
78
|
+
private windowsDisabledLogged;
|
|
78
79
|
constructor(sessions: SessionManager, config?: SwarmMonitorConfig);
|
|
79
80
|
/** Register an event handler for teammate lifecycle events. */
|
|
80
81
|
onEvent(handler: SwarmEventHandler): void;
|
|
81
82
|
private emitEvent;
|
|
83
|
+
private isWindowsPlatform;
|
|
84
|
+
private logWindowsDisabled;
|
|
82
85
|
/** Start the periodic scan loop. */
|
|
83
86
|
start(): void;
|
|
84
87
|
/** Stop the periodic scan loop. */
|
package/dist/swarm-monitor.js
CHANGED
|
@@ -28,6 +28,7 @@ export class SwarmMonitor {
|
|
|
28
28
|
lastResult = null;
|
|
29
29
|
timer = null;
|
|
30
30
|
eventHandlers = [];
|
|
31
|
+
windowsDisabledLogged = false;
|
|
31
32
|
constructor(sessions, config = DEFAULT_SWARM_CONFIG) {
|
|
32
33
|
this.sessions = sessions;
|
|
33
34
|
this.config = config;
|
|
@@ -46,8 +47,21 @@ export class SwarmMonitor {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
isWindowsPlatform() {
|
|
51
|
+
return process.platform === 'win32';
|
|
52
|
+
}
|
|
53
|
+
logWindowsDisabled() {
|
|
54
|
+
if (this.windowsDisabledLogged)
|
|
55
|
+
return;
|
|
56
|
+
console.info('SwarmMonitor disabled on Windows: tmux swarm sockets are not supported on this platform.');
|
|
57
|
+
this.windowsDisabledLogged = true;
|
|
58
|
+
}
|
|
49
59
|
/** Start the periodic scan loop. */
|
|
50
60
|
start() {
|
|
61
|
+
if (this.isWindowsPlatform()) {
|
|
62
|
+
this.logWindowsDisabled();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
51
65
|
if (this.running)
|
|
52
66
|
return;
|
|
53
67
|
this.running = true;
|
|
@@ -70,6 +84,16 @@ export class SwarmMonitor {
|
|
|
70
84
|
}
|
|
71
85
|
/** Run a single scan and return the result. */
|
|
72
86
|
async scan() {
|
|
87
|
+
if (this.isWindowsPlatform()) {
|
|
88
|
+
this.logWindowsDisabled();
|
|
89
|
+
this.lastResult = {
|
|
90
|
+
swarms: [],
|
|
91
|
+
totalSockets: 0,
|
|
92
|
+
totalTeammates: 0,
|
|
93
|
+
scannedAt: Date.now(),
|
|
94
|
+
};
|
|
95
|
+
return this.lastResult;
|
|
96
|
+
}
|
|
73
97
|
try {
|
|
74
98
|
const sockets = await this.discoverSwarmSockets();
|
|
75
99
|
// Issue #353: Inspect sockets in parallel to avoid N×timeout accumulation.
|
|
@@ -176,6 +200,17 @@ export class SwarmMonitor {
|
|
|
176
200
|
}
|
|
177
201
|
/** Inspect a single swarm socket and return swarm info. */
|
|
178
202
|
async inspectSwarmSocket(socketName) {
|
|
203
|
+
if (this.isWindowsPlatform()) {
|
|
204
|
+
const pid = this.extractPid(socketName);
|
|
205
|
+
return {
|
|
206
|
+
socketName,
|
|
207
|
+
pid,
|
|
208
|
+
parentSession: null,
|
|
209
|
+
teammates: [],
|
|
210
|
+
aggregatedStatus: 'no_teammates',
|
|
211
|
+
lastScannedAt: Date.now(),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
179
214
|
const pid = this.extractPid(socketName);
|
|
180
215
|
const teammates = await this.listSwarmWindows(socketName);
|
|
181
216
|
const parentSession = this.findParentSession(pid, teammates);
|
package/dist/tmux.js
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { homedir, tmpdir } from 'node:os';
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
14
|
import { computeProjectHash } from './path-utils.js';
|
|
15
|
+
import { secureFilePermissions } from './file-utils.js';
|
|
15
16
|
/** Shell-escape a string by wrapping in single quotes and escaping embedded single quotes. */
|
|
16
17
|
function shellEscape(s) {
|
|
17
18
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
@@ -420,6 +421,7 @@ export class TmuxManager {
|
|
|
420
421
|
return `export ${key}='${escaped}'`;
|
|
421
422
|
});
|
|
422
423
|
await fs.writeFile(tmpFile, lines.join('\n') + '\n', { mode: 0o600 });
|
|
424
|
+
await secureFilePermissions(tmpFile);
|
|
423
425
|
// Source the file and delete it — all in one command so the values
|
|
424
426
|
// appear in the process environment but not in the terminal history.
|
|
425
427
|
// The 'source' line is visible but only shows the temp file path, not the values.
|
|
@@ -489,6 +491,7 @@ export class TmuxManager {
|
|
|
489
491
|
return `export ${key}='${escaped}'`;
|
|
490
492
|
});
|
|
491
493
|
await fs.writeFile(tmpFile, lines.join('\n') + '\n', { mode: 0o600 });
|
|
494
|
+
await secureFilePermissions(tmpFile);
|
|
492
495
|
// Use sendKeysDirectInternal to avoid re-entering serialize()
|
|
493
496
|
const cmd = `source ${shellEscape(tmpFile)} && rm -f ${shellEscape(tmpFile)}`;
|
|
494
497
|
await this.sendKeysDirectInternal(windowId, cmd, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-bridge",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dev": "tsc && node dist/cli.js",
|
|
33
33
|
"prepublishOnly": "npm run build:dashboard && npm run build",
|
|
34
34
|
"test": "vitest run",
|
|
35
|
+
"test:smoke": "node scripts/uat-smoke.mjs",
|
|
35
36
|
"test:fault-harness": "vitest run src/__tests__/fault-injection-harness-901.test.ts"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|