aegis-bridge 0.1.0
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/LICENSE +21 -0
- package/README.md +404 -0
- package/dashboard/dist/assets/index-BoZwGLAx.css +32 -0
- package/dashboard/dist/assets/index-C61BkKH-.js +312 -0
- package/dashboard/dist/assets/index-C61BkKH-.js.map +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/api-contracts.d.ts +229 -0
- package/dist/api-contracts.js +7 -0
- package/dist/api-contracts.typecheck.d.ts +14 -0
- package/dist/api-contracts.typecheck.js +1 -0
- package/dist/api-error-envelope.d.ts +15 -0
- package/dist/api-error-envelope.js +80 -0
- package/dist/auth.d.ts +87 -0
- package/dist/auth.js +276 -0
- package/dist/channels/index.d.ts +8 -0
- package/dist/channels/index.js +8 -0
- package/dist/channels/manager.d.ts +47 -0
- package/dist/channels/manager.js +115 -0
- package/dist/channels/telegram-style.d.ts +118 -0
- package/dist/channels/telegram-style.js +202 -0
- package/dist/channels/telegram.d.ts +91 -0
- package/dist/channels/telegram.js +1518 -0
- package/dist/channels/types.d.ts +77 -0
- package/dist/channels/types.js +8 -0
- package/dist/channels/webhook.d.ts +60 -0
- package/dist/channels/webhook.js +216 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +252 -0
- package/dist/config.d.ts +90 -0
- package/dist/config.js +214 -0
- package/dist/consensus.d.ts +16 -0
- package/dist/consensus.js +19 -0
- package/dist/continuation-pointer.d.ts +11 -0
- package/dist/continuation-pointer.js +65 -0
- package/dist/diagnostics.d.ts +27 -0
- package/dist/diagnostics.js +95 -0
- package/dist/error-categories.d.ts +39 -0
- package/dist/error-categories.js +73 -0
- package/dist/events.d.ts +133 -0
- package/dist/events.js +389 -0
- package/dist/fault-injection.d.ts +29 -0
- package/dist/fault-injection.js +115 -0
- package/dist/file-utils.d.ts +2 -0
- package/dist/file-utils.js +37 -0
- package/dist/handshake.d.ts +60 -0
- package/dist/handshake.js +124 -0
- package/dist/hook-settings.d.ts +80 -0
- package/dist/hook-settings.js +272 -0
- package/dist/hook.d.ts +19 -0
- package/dist/hook.js +231 -0
- package/dist/hooks.d.ts +32 -0
- package/dist/hooks.js +364 -0
- package/dist/jsonl-watcher.d.ts +59 -0
- package/dist/jsonl-watcher.js +166 -0
- package/dist/logger.d.ts +35 -0
- package/dist/logger.js +65 -0
- package/dist/mcp-server.d.ts +123 -0
- package/dist/mcp-server.js +869 -0
- package/dist/memory-bridge.d.ts +27 -0
- package/dist/memory-bridge.js +137 -0
- package/dist/memory-routes.d.ts +3 -0
- package/dist/memory-routes.js +100 -0
- package/dist/metrics.d.ts +126 -0
- package/dist/metrics.js +286 -0
- package/dist/model-router.d.ts +53 -0
- package/dist/model-router.js +150 -0
- package/dist/monitor.d.ts +103 -0
- package/dist/monitor.js +820 -0
- package/dist/path-utils.d.ts +11 -0
- package/dist/path-utils.js +21 -0
- package/dist/permission-evaluator.d.ts +10 -0
- package/dist/permission-evaluator.js +48 -0
- package/dist/permission-guard.d.ts +51 -0
- package/dist/permission-guard.js +196 -0
- package/dist/permission-request-manager.d.ts +12 -0
- package/dist/permission-request-manager.js +36 -0
- package/dist/permission-routes.d.ts +7 -0
- package/dist/permission-routes.js +28 -0
- package/dist/pipeline.d.ts +97 -0
- package/dist/pipeline.js +291 -0
- package/dist/process-utils.d.ts +4 -0
- package/dist/process-utils.js +73 -0
- package/dist/question-manager.d.ts +54 -0
- package/dist/question-manager.js +80 -0
- package/dist/retry.d.ts +11 -0
- package/dist/retry.js +34 -0
- package/dist/safe-json.d.ts +12 -0
- package/dist/safe-json.js +22 -0
- package/dist/screenshot.d.ts +28 -0
- package/dist/screenshot.js +60 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +1973 -0
- package/dist/session-cleanup.d.ts +18 -0
- package/dist/session-cleanup.js +11 -0
- package/dist/session.d.ts +379 -0
- package/dist/session.js +1568 -0
- package/dist/shutdown-utils.d.ts +5 -0
- package/dist/shutdown-utils.js +24 -0
- package/dist/signal-cleanup-helper.d.ts +48 -0
- package/dist/signal-cleanup-helper.js +117 -0
- package/dist/sse-limiter.d.ts +47 -0
- package/dist/sse-limiter.js +61 -0
- package/dist/sse-writer.d.ts +31 -0
- package/dist/sse-writer.js +94 -0
- package/dist/ssrf.d.ts +102 -0
- package/dist/ssrf.js +267 -0
- package/dist/startup.d.ts +6 -0
- package/dist/startup.js +162 -0
- package/dist/suppress.d.ts +33 -0
- package/dist/suppress.js +79 -0
- package/dist/swarm-monitor.d.ts +117 -0
- package/dist/swarm-monitor.js +300 -0
- package/dist/template-store.d.ts +45 -0
- package/dist/template-store.js +142 -0
- package/dist/terminal-parser.d.ts +16 -0
- package/dist/terminal-parser.js +346 -0
- package/dist/tmux-capture-cache.d.ts +18 -0
- package/dist/tmux-capture-cache.js +34 -0
- package/dist/tmux.d.ts +183 -0
- package/dist/tmux.js +906 -0
- package/dist/tool-registry.d.ts +40 -0
- package/dist/tool-registry.js +83 -0
- package/dist/transcript.d.ts +63 -0
- package/dist/transcript.js +284 -0
- package/dist/utils/circular-buffer.d.ts +11 -0
- package/dist/utils/circular-buffer.js +37 -0
- package/dist/utils/redact-headers.d.ts +13 -0
- package/dist/utils/redact-headers.js +54 -0
- package/dist/validation.d.ts +406 -0
- package/dist/validation.js +415 -0
- package/dist/verification.d.ts +2 -0
- package/dist/verification.js +72 -0
- package/dist/worktree-lookup.d.ts +24 -0
- package/dist/worktree-lookup.js +71 -0
- package/dist/ws-terminal.d.ts +32 -0
- package/dist/ws-terminal.js +348 -0
- package/package.json +83 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — Configuration loader for Aegis.
|
|
3
|
+
*
|
|
4
|
+
* Priority (highest to lowest):
|
|
5
|
+
* 1. CLI argument --config <path>
|
|
6
|
+
* 2. ./aegis.config.json (cwd) — fallback: ./manus.config.json
|
|
7
|
+
* 3. ~/.aegis/config.json — fallback: ~/.manus/config.json
|
|
8
|
+
* 4. Defaults
|
|
9
|
+
*
|
|
10
|
+
* Environment variables override config file values.
|
|
11
|
+
* AEGIS_* env vars take priority; MANUS_* still supported for backward compat.
|
|
12
|
+
*/
|
|
13
|
+
export interface Config {
|
|
14
|
+
/** HTTP server port */
|
|
15
|
+
port: number;
|
|
16
|
+
/** HTTP server host */
|
|
17
|
+
host: string;
|
|
18
|
+
/** Bearer auth token (empty = no auth) */
|
|
19
|
+
authToken: string;
|
|
20
|
+
/** tmux session name */
|
|
21
|
+
tmuxSession: string;
|
|
22
|
+
/** Directory for bridge state (state.json, session_map.json) */
|
|
23
|
+
stateDir: string;
|
|
24
|
+
/** Directory where Claude Code stores projects (~/.claude/projects) */
|
|
25
|
+
claudeProjectsDir: string;
|
|
26
|
+
/** Max session age in milliseconds */
|
|
27
|
+
maxSessionAgeMs: number;
|
|
28
|
+
/** Reaper check interval in milliseconds */
|
|
29
|
+
reaperIntervalMs: number;
|
|
30
|
+
/** Continuation pointer TTL in milliseconds (Issue #900). */
|
|
31
|
+
continuationPointerTtlMs: number;
|
|
32
|
+
/** Telegram bot token */
|
|
33
|
+
tgBotToken: string;
|
|
34
|
+
/** Telegram group chat ID */
|
|
35
|
+
tgGroupId: string;
|
|
36
|
+
/** Allowed Telegram user IDs for inbound commands (empty = allow all) */
|
|
37
|
+
tgAllowedUsers: number[];
|
|
38
|
+
/** TTL for Telegram forum topics after session end, in milliseconds. */
|
|
39
|
+
tgTopicTtlMs: number;
|
|
40
|
+
/** Webhook URLs (comma-separated or array) */
|
|
41
|
+
webhooks: string[];
|
|
42
|
+
/** Default env vars injected into every CC session (e.g. model overrides, API keys).
|
|
43
|
+
* Per-session env vars from the API merge on top (per-session wins). */
|
|
44
|
+
defaultSessionEnv: Record<string, string>;
|
|
45
|
+
/** Default permission mode for new sessions (default: "bypassPermissions").
|
|
46
|
+
* Aegis is headless — there is no human at the TTY to approve prompts.
|
|
47
|
+
* Set explicitly to "default" if approval gating is needed.
|
|
48
|
+
* Values: "default" | "plan" | "acceptEdits" | "bypassPermissions" | "dontAsk" | "auto" */
|
|
49
|
+
defaultPermissionMode: string;
|
|
50
|
+
/** Stall threshold for monitor (ms). */
|
|
51
|
+
stallThresholdMs: number;
|
|
52
|
+
/** Maximum total concurrent SSE connections (default: 100). Env: AEGIS_SSE_MAX_CONNECTIONS */
|
|
53
|
+
sseMaxConnections: number;
|
|
54
|
+
/** Maximum concurrent SSE connections per client IP (default: 10). Env: AEGIS_SSE_MAX_PER_IP */
|
|
55
|
+
sseMaxPerIp: number;
|
|
56
|
+
/** Allowed working directories for session creation (Issue #349).
|
|
57
|
+
* Empty array = all directories allowed (backward compatible).
|
|
58
|
+
* Paths are resolved and symlink-resolved before checking. */
|
|
59
|
+
allowedWorkDirs: string[];
|
|
60
|
+
/** Memory bridge: key/value store for cross-session context (default: disabled). */
|
|
61
|
+
memoryBridge: {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
persistPath?: string;
|
|
64
|
+
reaperIntervalMs?: number;
|
|
65
|
+
};
|
|
66
|
+
/** Issue #884: Enable worktree-aware continuation metadata lookup (default: false).
|
|
67
|
+
* When true, Aegis fans out to sibling worktree project dirs when the primary
|
|
68
|
+
* directory lookup fails to find a session file. */
|
|
69
|
+
worktreeAwareContinuation: boolean;
|
|
70
|
+
/** Issue #884: Additional Claude projects directories to search during worktree fanout.
|
|
71
|
+
* Paths are expanded (~) and checked for existence before searching. */
|
|
72
|
+
worktreeSiblingDirs: string[];
|
|
73
|
+
/** Issue #740: Verification Protocol — auto run quality gate after session ends.
|
|
74
|
+
* When enabled, Aegis runs tsc + build + test after a Stop hook and emits
|
|
75
|
+
* results via SSE (event: 'verification'). */
|
|
76
|
+
verificationProtocol: {
|
|
77
|
+
/** Auto-run verification when Stop hook fires (default: false). */
|
|
78
|
+
autoVerifyOnStop: boolean;
|
|
79
|
+
/** Run only critical checks: tsc + build (skip slow tests). Default: false = full. */
|
|
80
|
+
criticalOnly: boolean;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Compute stall threshold from env var or default (Issue #392).
|
|
84
|
+
* If CLAUDE_STREAM_IDLE_TIMEOUT_MS is set, uses Math.max(120000, parseInt(val) * 1.5).
|
|
85
|
+
* Otherwise defaults to 2 minutes (120000ms). */
|
|
86
|
+
export declare function computeStallThreshold(): number;
|
|
87
|
+
/** Load and merge configuration from all sources */
|
|
88
|
+
export declare function loadConfig(): Promise<Config>;
|
|
89
|
+
/** Get config without async file loading (for tests or synchronous contexts) */
|
|
90
|
+
export declare function getConfig(): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — Configuration loader for Aegis.
|
|
3
|
+
*
|
|
4
|
+
* Priority (highest to lowest):
|
|
5
|
+
* 1. CLI argument --config <path>
|
|
6
|
+
* 2. ./aegis.config.json (cwd) — fallback: ./manus.config.json
|
|
7
|
+
* 3. ~/.aegis/config.json — fallback: ~/.manus/config.json
|
|
8
|
+
* 4. Defaults
|
|
9
|
+
*
|
|
10
|
+
* Environment variables override config file values.
|
|
11
|
+
* AEGIS_* env vars take priority; MANUS_* still supported for backward compat.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { resolve, join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { parseIntSafe } from './validation.js';
|
|
18
|
+
/** Compute stall threshold from env var or default (Issue #392).
|
|
19
|
+
* If CLAUDE_STREAM_IDLE_TIMEOUT_MS is set, uses Math.max(120000, parseInt(val) * 1.5).
|
|
20
|
+
* Otherwise defaults to 2 minutes (120000ms). */
|
|
21
|
+
export function computeStallThreshold() {
|
|
22
|
+
const env = process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS;
|
|
23
|
+
if (env) {
|
|
24
|
+
return Math.max(120_000, Math.round(parseInt(env, 10) * 1.5));
|
|
25
|
+
}
|
|
26
|
+
return 2 * 60 * 1000;
|
|
27
|
+
}
|
|
28
|
+
/** Default configuration values */
|
|
29
|
+
const defaults = {
|
|
30
|
+
port: 9100,
|
|
31
|
+
host: '127.0.0.1',
|
|
32
|
+
authToken: '',
|
|
33
|
+
tmuxSession: 'aegis',
|
|
34
|
+
stateDir: join(homedir(), '.aegis'),
|
|
35
|
+
claudeProjectsDir: join(homedir(), '.claude', 'projects'),
|
|
36
|
+
maxSessionAgeMs: 2 * 60 * 60 * 1000, // 2 hours
|
|
37
|
+
reaperIntervalMs: 5 * 60 * 1000, // 5 minutes
|
|
38
|
+
continuationPointerTtlMs: 24 * 60 * 60 * 1000, // 24 hours
|
|
39
|
+
tgBotToken: '',
|
|
40
|
+
tgGroupId: '',
|
|
41
|
+
tgAllowedUsers: [],
|
|
42
|
+
tgTopicTtlMs: 24 * 60 * 60 * 1000,
|
|
43
|
+
webhooks: [],
|
|
44
|
+
defaultSessionEnv: {},
|
|
45
|
+
defaultPermissionMode: 'default',
|
|
46
|
+
stallThresholdMs: computeStallThreshold(),
|
|
47
|
+
sseMaxConnections: 100,
|
|
48
|
+
sseMaxPerIp: 10,
|
|
49
|
+
allowedWorkDirs: [],
|
|
50
|
+
worktreeAwareContinuation: false,
|
|
51
|
+
memoryBridge: { enabled: false },
|
|
52
|
+
worktreeSiblingDirs: [],
|
|
53
|
+
verificationProtocol: { autoVerifyOnStop: false, criticalOnly: false },
|
|
54
|
+
};
|
|
55
|
+
/** Parse CLI args for --config flag */
|
|
56
|
+
function getConfigPathFromArgv() {
|
|
57
|
+
const idx = process.argv.indexOf('--config');
|
|
58
|
+
if (idx !== -1 && idx + 1 < process.argv.length) {
|
|
59
|
+
return resolve(process.argv[idx + 1]);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Find and load config file from possible locations.
|
|
64
|
+
* Checks aegis paths first, falls back to manus paths for backward compat.
|
|
65
|
+
*/
|
|
66
|
+
async function loadConfigFile() {
|
|
67
|
+
const locations = [
|
|
68
|
+
getConfigPathFromArgv(),
|
|
69
|
+
// New aegis paths (preferred)
|
|
70
|
+
resolve('aegis.config.json'),
|
|
71
|
+
join(homedir(), '.aegis', 'config.json'),
|
|
72
|
+
// Legacy manus paths (backward compat)
|
|
73
|
+
resolve('manus.config.json'),
|
|
74
|
+
join(homedir(), '.manus', 'config.json'),
|
|
75
|
+
].filter(Boolean);
|
|
76
|
+
for (const path of locations) {
|
|
77
|
+
if (existsSync(path)) {
|
|
78
|
+
try {
|
|
79
|
+
const data = await readFile(path, 'utf-8');
|
|
80
|
+
const parsed = JSON.parse(data);
|
|
81
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
82
|
+
console.warn(`Config file ${path} is not a JSON object, ignoring`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Expand ~ in paths
|
|
86
|
+
if (typeof parsed.stateDir === 'string') {
|
|
87
|
+
parsed.stateDir = expandTilde(parsed.stateDir);
|
|
88
|
+
}
|
|
89
|
+
if (typeof parsed.claudeProjectsDir === 'string') {
|
|
90
|
+
parsed.claudeProjectsDir = expandTilde(parsed.claudeProjectsDir);
|
|
91
|
+
}
|
|
92
|
+
// Log if using legacy path
|
|
93
|
+
if (path.includes('manus')) {
|
|
94
|
+
console.log(`Config: loaded from legacy path ${path} — consider migrating to aegis paths`);
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn(`Failed to parse config file ${path}:`, e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
/** Expand ~ to homedir in path strings */
|
|
106
|
+
function expandTilde(path) {
|
|
107
|
+
if (path.startsWith('~/')) {
|
|
108
|
+
return join(homedir(), path.slice(2));
|
|
109
|
+
}
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
/** Apply environment variable overrides.
|
|
113
|
+
* AEGIS_* vars take priority over MANUS_* (backward compat).
|
|
114
|
+
*/
|
|
115
|
+
function applyEnvOverrides(config) {
|
|
116
|
+
// AEGIS_* (new, preferred) and MANUS_* (legacy, backward compat)
|
|
117
|
+
const envMappings = [
|
|
118
|
+
{ aegis: 'AEGIS_PORT', manus: 'MANUS_PORT', key: 'port' },
|
|
119
|
+
{ aegis: 'AEGIS_HOST', manus: 'MANUS_HOST', key: 'host' },
|
|
120
|
+
{ aegis: 'AEGIS_AUTH_TOKEN', manus: 'MANUS_AUTH_TOKEN', key: 'authToken' },
|
|
121
|
+
{ aegis: 'AEGIS_TMUX_SESSION', manus: 'MANUS_TMUX_SESSION', key: 'tmuxSession' },
|
|
122
|
+
{ aegis: 'AEGIS_STATE_DIR', manus: 'MANUS_STATE_DIR', key: 'stateDir' },
|
|
123
|
+
{ aegis: 'AEGIS_CLAUDE_PROJECTS_DIR', manus: 'MANUS_CLAUDE_PROJECTS_DIR', key: 'claudeProjectsDir' },
|
|
124
|
+
{ aegis: 'AEGIS_MAX_SESSION_AGE_MS', manus: 'MANUS_MAX_SESSION_AGE_MS', key: 'maxSessionAgeMs' },
|
|
125
|
+
{ aegis: 'AEGIS_REAPER_INTERVAL_MS', manus: 'MANUS_REAPER_INTERVAL_MS', key: 'reaperIntervalMs' },
|
|
126
|
+
{ aegis: 'AEGIS_CONTINUATION_POINTER_TTL_MS', manus: 'MANUS_CONTINUATION_POINTER_TTL_MS', key: 'continuationPointerTtlMs' },
|
|
127
|
+
{ aegis: 'AEGIS_TG_TOKEN', manus: 'MANUS_TG_TOKEN', key: 'tgBotToken' },
|
|
128
|
+
{ aegis: 'AEGIS_TG_GROUP', manus: 'MANUS_TG_GROUP', key: 'tgGroupId' },
|
|
129
|
+
{ aegis: 'AEGIS_TG_ALLOWED_USERS', manus: 'MANUS_TG_ALLOWED_USERS', key: 'tgAllowedUsers' },
|
|
130
|
+
{ aegis: 'AEGIS_TG_TOPIC_TTL_MS', manus: 'MANUS_TG_TOPIC_TTL_MS', key: 'tgTopicTtlMs' },
|
|
131
|
+
{ aegis: 'AEGIS_WEBHOOKS', manus: 'MANUS_WEBHOOKS', key: 'webhooks' },
|
|
132
|
+
{ aegis: 'AEGIS_SSE_MAX_CONNECTIONS', manus: 'MANUS_SSE_MAX_CONNECTIONS', key: 'sseMaxConnections' },
|
|
133
|
+
{ aegis: 'AEGIS_SSE_MAX_PER_IP', manus: 'MANUS_SSE_MAX_PER_IP', key: 'sseMaxPerIp' },
|
|
134
|
+
];
|
|
135
|
+
for (const { aegis, manus, key } of envMappings) {
|
|
136
|
+
// AEGIS_* takes priority over MANUS_*
|
|
137
|
+
const value = process.env[aegis] ?? process.env[manus];
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
switch (key) {
|
|
141
|
+
case 'port':
|
|
142
|
+
case 'maxSessionAgeMs':
|
|
143
|
+
case 'reaperIntervalMs':
|
|
144
|
+
case 'continuationPointerTtlMs':
|
|
145
|
+
case 'tgTopicTtlMs':
|
|
146
|
+
case 'sseMaxConnections':
|
|
147
|
+
case 'sseMaxPerIp':
|
|
148
|
+
config[key] = parseIntSafe(value, config[key]);
|
|
149
|
+
break;
|
|
150
|
+
case 'webhooks':
|
|
151
|
+
// Support comma-separated webhooks
|
|
152
|
+
config[key] = value.includes(',')
|
|
153
|
+
? value.split(',').map(s => s.trim())
|
|
154
|
+
: [value];
|
|
155
|
+
break;
|
|
156
|
+
case 'tgAllowedUsers':
|
|
157
|
+
config[key] = value.split(',').map(s => Number(s.trim())).filter(n => !isNaN(n) && n > 0);
|
|
158
|
+
break;
|
|
159
|
+
// All remaining env-mapped keys are string-typed — assign directly.
|
|
160
|
+
case 'host':
|
|
161
|
+
case 'authToken':
|
|
162
|
+
case 'tmuxSession':
|
|
163
|
+
case 'stateDir':
|
|
164
|
+
case 'claudeProjectsDir':
|
|
165
|
+
case 'tgBotToken':
|
|
166
|
+
case 'tgGroupId':
|
|
167
|
+
config[key] = value;
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
// Skip complex types (Record<string,string>) that can't be set from a single env var
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return config;
|
|
175
|
+
}
|
|
176
|
+
/** Resolve the state directory.
|
|
177
|
+
* If ~/.aegis doesn't exist but ~/.manus does, use ~/.manus for backward compat.
|
|
178
|
+
*/
|
|
179
|
+
function resolveStateDir(config) {
|
|
180
|
+
const aegisDir = join(homedir(), '.aegis');
|
|
181
|
+
const manusDir = join(homedir(), '.manus');
|
|
182
|
+
// If stateDir is the default aegis path but doesn't exist, check for legacy manus path
|
|
183
|
+
if (config.stateDir === aegisDir && !existsSync(aegisDir) && existsSync(manusDir)) {
|
|
184
|
+
console.log(`Config: using legacy state dir ${manusDir} — consider migrating to ${aegisDir}`);
|
|
185
|
+
config.stateDir = manusDir;
|
|
186
|
+
}
|
|
187
|
+
return config;
|
|
188
|
+
}
|
|
189
|
+
/** Load and merge configuration from all sources */
|
|
190
|
+
export async function loadConfig() {
|
|
191
|
+
const fileConfig = await loadConfigFile();
|
|
192
|
+
let config = { ...defaults, ...fileConfig };
|
|
193
|
+
config = applyEnvOverrides(config);
|
|
194
|
+
config = resolveStateDir(config);
|
|
195
|
+
// Issue #349: Resolve allowedWorkDirs entries via realpath so symlink targets match
|
|
196
|
+
if (config.allowedWorkDirs.length > 0) {
|
|
197
|
+
config.allowedWorkDirs = await Promise.all(config.allowedWorkDirs.map(async (dir) => {
|
|
198
|
+
try {
|
|
199
|
+
return await realpath(resolve(dir));
|
|
200
|
+
}
|
|
201
|
+
catch { /* dir does not exist yet — use unresolved path */
|
|
202
|
+
return resolve(dir);
|
|
203
|
+
}
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
return config;
|
|
207
|
+
}
|
|
208
|
+
/** Get config without async file loading (for tests or synchronous contexts) */
|
|
209
|
+
export function getConfig() {
|
|
210
|
+
// This returns defaults + env overrides only (no file loading)
|
|
211
|
+
let config = { ...defaults };
|
|
212
|
+
config = applyEnvOverrides(config);
|
|
213
|
+
return config;
|
|
214
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ConsensusFocusArea = 'correctness' | 'security' | 'performance';
|
|
2
|
+
export interface ConsensusRequest {
|
|
3
|
+
id: string;
|
|
4
|
+
targetSessionId: string;
|
|
5
|
+
reviewerIds: string[];
|
|
6
|
+
focusAreas: ConsensusFocusArea[];
|
|
7
|
+
status: 'running' | 'completed' | 'failed';
|
|
8
|
+
createdAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ConsensusReview {
|
|
11
|
+
reviewerId: string;
|
|
12
|
+
focusArea: ConsensusFocusArea;
|
|
13
|
+
findings: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function buildConsensusPrompt(targetSessionId: string, focusArea: ConsensusFocusArea): string;
|
|
16
|
+
export declare function mergeConsensusFindings(reviews: ConsensusReview[]): string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function buildConsensusPrompt(targetSessionId, focusArea) {
|
|
2
|
+
return [
|
|
3
|
+
`Review Aegis session ${targetSessionId}.`,
|
|
4
|
+
`Focus area: ${focusArea}.`,
|
|
5
|
+
'Return concise findings ordered by severity.',
|
|
6
|
+
'Prefer concrete regressions, risks, and missing verification.',
|
|
7
|
+
].join(' ');
|
|
8
|
+
}
|
|
9
|
+
export function mergeConsensusFindings(reviews) {
|
|
10
|
+
const merged = new Set();
|
|
11
|
+
for (const review of reviews) {
|
|
12
|
+
for (const finding of review.findings) {
|
|
13
|
+
const normalized = finding.trim();
|
|
14
|
+
if (normalized)
|
|
15
|
+
merged.add(normalized);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return Array.from(merged.values());
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import { sessionMapEntrySchema } from './validation.js';
|
|
3
|
+
export type ContinuationPointerEntry = z.infer<typeof sessionMapEntrySchema>;
|
|
4
|
+
/**
|
|
5
|
+
* Read continuation pointers with schema validation + TTL cleanup.
|
|
6
|
+
*
|
|
7
|
+
* Backward compatible behavior:
|
|
8
|
+
* - Legacy entries without expires_at are accepted and normalized.
|
|
9
|
+
* - Corrupt files do not throw; they are reset to an empty map.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadContinuationPointers(sessionMapFile: string, ttlMs: number, nowMs?: number): Promise<Record<string, ContinuationPointerEntry>>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { sessionMapEntrySchema } from './validation.js';
|
|
4
|
+
import { safeJsonParse } from './safe-json.js';
|
|
5
|
+
function computeExpiresAt(entry, ttlMs) {
|
|
6
|
+
if (typeof entry.expires_at === 'number') {
|
|
7
|
+
return entry.expires_at;
|
|
8
|
+
}
|
|
9
|
+
return entry.written_at + ttlMs;
|
|
10
|
+
}
|
|
11
|
+
async function persistPointerMap(sessionMapFile, mapData) {
|
|
12
|
+
const tmpFile = `${sessionMapFile}.tmp`;
|
|
13
|
+
await writeFile(tmpFile, JSON.stringify(mapData, null, 2));
|
|
14
|
+
await rename(tmpFile, sessionMapFile);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read continuation pointers with schema validation + TTL cleanup.
|
|
18
|
+
*
|
|
19
|
+
* Backward compatible behavior:
|
|
20
|
+
* - Legacy entries without expires_at are accepted and normalized.
|
|
21
|
+
* - Corrupt files do not throw; they are reset to an empty map.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadContinuationPointers(sessionMapFile, ttlMs, nowMs = Date.now()) {
|
|
24
|
+
if (!existsSync(sessionMapFile))
|
|
25
|
+
return {};
|
|
26
|
+
let parsed;
|
|
27
|
+
const raw = await readFile(sessionMapFile, 'utf-8');
|
|
28
|
+
const parsedResult = safeJsonParse(raw, 'Continuation pointer map');
|
|
29
|
+
if (!parsedResult.ok) {
|
|
30
|
+
await persistPointerMap(sessionMapFile, {});
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
parsed = parsedResult.data;
|
|
34
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
35
|
+
await persistPointerMap(sessionMapFile, {});
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const cleaned = {};
|
|
39
|
+
let changed = false;
|
|
40
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
41
|
+
const entryResult = sessionMapEntrySchema.safeParse(value);
|
|
42
|
+
if (!entryResult.success) {
|
|
43
|
+
changed = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const entry = entryResult.data;
|
|
47
|
+
const expiresAt = computeExpiresAt(entry, ttlMs);
|
|
48
|
+
if (expiresAt <= nowMs) {
|
|
49
|
+
changed = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (entry.expires_at !== expiresAt || entry.schema_version !== 1) {
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
cleaned[key] = {
|
|
56
|
+
...entry,
|
|
57
|
+
schema_version: 1,
|
|
58
|
+
expires_at: expiresAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (changed) {
|
|
62
|
+
await persistPointerMap(sessionMapFile, cleaned);
|
|
63
|
+
}
|
|
64
|
+
return cleaned;
|
|
65
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diagnostics.ts - no-PII diagnostics event stream with bounded in-memory buffer.
|
|
3
|
+
*/
|
|
4
|
+
export type DiagnosticsLevel = 'info' | 'warn' | 'error';
|
|
5
|
+
export interface DiagnosticsEvent {
|
|
6
|
+
event: string;
|
|
7
|
+
level: DiagnosticsLevel;
|
|
8
|
+
component: string;
|
|
9
|
+
operation: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
errorCode?: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
attributes: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_DIAGNOSTICS_BUFFER_SIZE = 100;
|
|
16
|
+
export declare function sanitizeDiagnosticsAttributes(attributes: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
17
|
+
export declare class DiagnosticsBus {
|
|
18
|
+
private readonly maxEntries;
|
|
19
|
+
private readonly emitter;
|
|
20
|
+
private readonly buffer;
|
|
21
|
+
constructor(maxEntries?: number);
|
|
22
|
+
emit(event: DiagnosticsEvent): void;
|
|
23
|
+
subscribe(handler: (event: DiagnosticsEvent) => void): () => void;
|
|
24
|
+
getRecent(limit?: number): DiagnosticsEvent[];
|
|
25
|
+
clear(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare const diagnosticsBus: DiagnosticsBus;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diagnostics.ts - no-PII diagnostics event stream with bounded in-memory buffer.
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
export const DEFAULT_DIAGNOSTICS_BUFFER_SIZE = 100;
|
|
6
|
+
const MAX_DIAGNOSTICS_STRING_LENGTH = 200;
|
|
7
|
+
const MAX_SANITIZE_DEPTH = 4;
|
|
8
|
+
const FORBIDDEN_KEY_FRAGMENTS = [
|
|
9
|
+
'token',
|
|
10
|
+
'password',
|
|
11
|
+
'secret',
|
|
12
|
+
'authorization',
|
|
13
|
+
'cookie',
|
|
14
|
+
'auth',
|
|
15
|
+
'api_key',
|
|
16
|
+
'apikey',
|
|
17
|
+
'prompt',
|
|
18
|
+
'transcript',
|
|
19
|
+
'payload',
|
|
20
|
+
'workdir',
|
|
21
|
+
];
|
|
22
|
+
function isForbiddenAttribute(key) {
|
|
23
|
+
const normalized = key.toLowerCase();
|
|
24
|
+
return FORBIDDEN_KEY_FRAGMENTS.some(fragment => normalized.includes(fragment));
|
|
25
|
+
}
|
|
26
|
+
function sanitizeValue(value, depth = 0) {
|
|
27
|
+
if (depth > MAX_SANITIZE_DEPTH)
|
|
28
|
+
return '[TRUNCATED]';
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
return value.length > MAX_DIAGNOSTICS_STRING_LENGTH
|
|
31
|
+
? `${value.slice(0, MAX_DIAGNOSTICS_STRING_LENGTH)}...`
|
|
32
|
+
: value;
|
|
33
|
+
}
|
|
34
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map(item => sanitizeValue(item, depth + 1));
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === 'object') {
|
|
41
|
+
const sanitizedObject = {};
|
|
42
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
43
|
+
if (isForbiddenAttribute(key))
|
|
44
|
+
continue;
|
|
45
|
+
sanitizedObject[key] = sanitizeValue(nested, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
return sanitizedObject;
|
|
48
|
+
}
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
export function sanitizeDiagnosticsAttributes(attributes) {
|
|
54
|
+
if (!attributes)
|
|
55
|
+
return {};
|
|
56
|
+
const sanitized = sanitizeValue(attributes);
|
|
57
|
+
return (typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized))
|
|
58
|
+
? sanitized
|
|
59
|
+
: {};
|
|
60
|
+
}
|
|
61
|
+
function sanitizeDiagnosticsEvent(event) {
|
|
62
|
+
return {
|
|
63
|
+
...event,
|
|
64
|
+
attributes: sanitizeDiagnosticsAttributes(event.attributes),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export class DiagnosticsBus {
|
|
68
|
+
maxEntries;
|
|
69
|
+
emitter = new EventEmitter();
|
|
70
|
+
buffer = [];
|
|
71
|
+
constructor(maxEntries = DEFAULT_DIAGNOSTICS_BUFFER_SIZE) {
|
|
72
|
+
this.maxEntries = maxEntries;
|
|
73
|
+
}
|
|
74
|
+
emit(event) {
|
|
75
|
+
const sanitizedEvent = sanitizeDiagnosticsEvent(event);
|
|
76
|
+
this.buffer.push(sanitizedEvent);
|
|
77
|
+
if (this.buffer.length > this.maxEntries) {
|
|
78
|
+
this.buffer.splice(0, this.buffer.length - this.maxEntries);
|
|
79
|
+
}
|
|
80
|
+
this.emitter.emit('event', sanitizedEvent);
|
|
81
|
+
}
|
|
82
|
+
subscribe(handler) {
|
|
83
|
+
this.emitter.on('event', handler);
|
|
84
|
+
return () => this.emitter.off('event', handler);
|
|
85
|
+
}
|
|
86
|
+
getRecent(limit = this.maxEntries) {
|
|
87
|
+
if (limit <= 0)
|
|
88
|
+
return [];
|
|
89
|
+
return this.buffer.slice(-Math.min(limit, this.maxEntries));
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
this.buffer.length = 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export const diagnosticsBus = new DiagnosticsBus();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-categories.ts — Structured error categorization and retry guidance.
|
|
3
|
+
*
|
|
4
|
+
* Issue #701: Provides an ErrorCode enum, categorize() function to inspect
|
|
5
|
+
* unknown errors and return structured metadata, and shouldRetry() helper.
|
|
6
|
+
*/
|
|
7
|
+
/** String enum of Aegis error codes. */
|
|
8
|
+
export declare enum ErrorCode {
|
|
9
|
+
/** Session not found, already deleted, or in wrong state. */
|
|
10
|
+
SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
|
|
11
|
+
/** Session creation failed (tmux window, CC launch). */
|
|
12
|
+
SESSION_CREATE_FAILED = "SESSION_CREATE_FAILED",
|
|
13
|
+
/** Permission request was rejected by the user. */
|
|
14
|
+
PERMISSION_REJECTED = "PERMISSION_REJECTED",
|
|
15
|
+
/** Tmux command timed out. */
|
|
16
|
+
TMUX_TIMEOUT = "TMUX_TIMEOUT",
|
|
17
|
+
/** Tmux operation failed (non-timeout). */
|
|
18
|
+
TMUX_ERROR = "TMUX_ERROR",
|
|
19
|
+
/** Request body or parameter failed validation. */
|
|
20
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
21
|
+
/** Authentication failed (missing/invalid token). */
|
|
22
|
+
AUTH_ERROR = "AUTH_ERROR",
|
|
23
|
+
/** Rate limit exceeded. */
|
|
24
|
+
RATE_LIMITED = "RATE_LIMITED",
|
|
25
|
+
/** Network or I/O error (transient). */
|
|
26
|
+
NETWORK_ERROR = "NETWORK_ERROR",
|
|
27
|
+
/** Unexpected internal error. */
|
|
28
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
29
|
+
}
|
|
30
|
+
/** Structured result returned by categorize(). */
|
|
31
|
+
export interface CategorizedError {
|
|
32
|
+
code: ErrorCode;
|
|
33
|
+
message: string;
|
|
34
|
+
retryable: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Inspect an unknown error and return a structured categorization. */
|
|
37
|
+
export declare function categorize(error: unknown): CategorizedError;
|
|
38
|
+
/** Return true if the error is worth retrying. */
|
|
39
|
+
export declare function shouldRetry(error: unknown): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-categories.ts — Structured error categorization and retry guidance.
|
|
3
|
+
*
|
|
4
|
+
* Issue #701: Provides an ErrorCode enum, categorize() function to inspect
|
|
5
|
+
* unknown errors and return structured metadata, and shouldRetry() helper.
|
|
6
|
+
*/
|
|
7
|
+
import { TmuxTimeoutError } from './tmux.js';
|
|
8
|
+
/** String enum of Aegis error codes. */
|
|
9
|
+
export var ErrorCode;
|
|
10
|
+
(function (ErrorCode) {
|
|
11
|
+
/** Session not found, already deleted, or in wrong state. */
|
|
12
|
+
ErrorCode["SESSION_NOT_FOUND"] = "SESSION_NOT_FOUND";
|
|
13
|
+
/** Session creation failed (tmux window, CC launch). */
|
|
14
|
+
ErrorCode["SESSION_CREATE_FAILED"] = "SESSION_CREATE_FAILED";
|
|
15
|
+
/** Permission request was rejected by the user. */
|
|
16
|
+
ErrorCode["PERMISSION_REJECTED"] = "PERMISSION_REJECTED";
|
|
17
|
+
/** Tmux command timed out. */
|
|
18
|
+
ErrorCode["TMUX_TIMEOUT"] = "TMUX_TIMEOUT";
|
|
19
|
+
/** Tmux operation failed (non-timeout). */
|
|
20
|
+
ErrorCode["TMUX_ERROR"] = "TMUX_ERROR";
|
|
21
|
+
/** Request body or parameter failed validation. */
|
|
22
|
+
ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
23
|
+
/** Authentication failed (missing/invalid token). */
|
|
24
|
+
ErrorCode["AUTH_ERROR"] = "AUTH_ERROR";
|
|
25
|
+
/** Rate limit exceeded. */
|
|
26
|
+
ErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
|
|
27
|
+
/** Network or I/O error (transient). */
|
|
28
|
+
ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
29
|
+
/** Unexpected internal error. */
|
|
30
|
+
ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
31
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
32
|
+
/** Inspect an unknown error and return a structured categorization. */
|
|
33
|
+
export function categorize(error) {
|
|
34
|
+
// 1. Known typed errors
|
|
35
|
+
if (error instanceof TmuxTimeoutError) {
|
|
36
|
+
return { code: ErrorCode.TMUX_TIMEOUT, message: error.message, retryable: true };
|
|
37
|
+
}
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
const msg = error.message;
|
|
40
|
+
const lower = msg.toLowerCase();
|
|
41
|
+
// 2. Message-based heuristics for common Aegis error patterns
|
|
42
|
+
if (lower.includes('session not found') || lower.includes('no session with id')) {
|
|
43
|
+
return { code: ErrorCode.SESSION_NOT_FOUND, message: msg, retryable: false };
|
|
44
|
+
}
|
|
45
|
+
if (lower.includes('permission denied') || lower.includes('permission rejected')) {
|
|
46
|
+
return { code: ErrorCode.PERMISSION_REJECTED, message: msg, retryable: false };
|
|
47
|
+
}
|
|
48
|
+
if (lower.includes('unauthorized') || lower.includes('invalid token') || lower.includes('authentication')) {
|
|
49
|
+
return { code: ErrorCode.AUTH_ERROR, message: msg, retryable: false };
|
|
50
|
+
}
|
|
51
|
+
if (lower.includes('rate limit') || lower.includes('too many requests')) {
|
|
52
|
+
return { code: ErrorCode.RATE_LIMITED, message: msg, retryable: true };
|
|
53
|
+
}
|
|
54
|
+
if (lower.includes('validation') || lower.includes('invalid ') || lower.includes('required')) {
|
|
55
|
+
return { code: ErrorCode.VALIDATION_ERROR, message: msg, retryable: false };
|
|
56
|
+
}
|
|
57
|
+
if (lower.includes('econnrefused') || lower.includes('econnreset') || lower.includes('etimedout') || lower.includes('fetch failed')) {
|
|
58
|
+
return { code: ErrorCode.NETWORK_ERROR, message: msg, retryable: true };
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('tmux')) {
|
|
61
|
+
return { code: ErrorCode.TMUX_ERROR, message: msg, retryable: true };
|
|
62
|
+
}
|
|
63
|
+
// 3. Generic Error fallback
|
|
64
|
+
return { code: ErrorCode.INTERNAL_ERROR, message: msg, retryable: false };
|
|
65
|
+
}
|
|
66
|
+
// 4. Non-Error values
|
|
67
|
+
const msg = typeof error === 'string' ? error : String(error);
|
|
68
|
+
return { code: ErrorCode.INTERNAL_ERROR, message: msg, retryable: false };
|
|
69
|
+
}
|
|
70
|
+
/** Return true if the error is worth retrying. */
|
|
71
|
+
export function shouldRetry(error) {
|
|
72
|
+
return categorize(error).retryable;
|
|
73
|
+
}
|