ats-daemon 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MFS-code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # ats-daemon
2
+
3
+ Companion daemon for [Agent Testing Suite](https://github.com/MFS-code/ralph-testing-suite). Runs on your local machine, connects to the cloud server, manages Docker containers for agent test runs, and streams results back in real time.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Node.js 20+**
8
+ - **Docker** installed and running (Docker Desktop or Docker Engine)
9
+ - The `agent-runner:latest` Docker image built locally (see [setup instructions](https://github.com/MFS-code/ralph-testing-suite#quick-start))
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g ats-daemon
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ 1. Open the Agent Testing Suite web UI
20
+ 2. Create a test run and copy the pairing token
21
+ 3. Run the daemon:
22
+
23
+ ```bash
24
+ ats-daemon --token <YOUR_PAIRING_TOKEN>
25
+ ```
26
+
27
+ The daemon will:
28
+ - Verify Docker is installed and running
29
+ - Connect to the server via WebSocket
30
+ - Wait for test commands
31
+ - Spin up Docker containers for each agent setup
32
+ - Stream container output back to the server for live metrics
33
+
34
+ ## Options
35
+
36
+ ```
37
+ USAGE:
38
+ ats-daemon --token <TOKEN> [OPTIONS]
39
+
40
+ REQUIRED:
41
+ --token <TOKEN> Pairing token from the web UI
42
+
43
+ OPTIONS:
44
+ --server <URL> Server URL (default: ws://localhost:3001)
45
+ --help, -h Show this help message
46
+ --version, -v Show version
47
+ ```
48
+
49
+ ## API Keys
50
+
51
+ API keys can be provided in two ways:
52
+
53
+ 1. **Environment variables** on the machine running the daemon:
54
+
55
+ ```bash
56
+ export ANTHROPIC_API_KEY=sk-ant-...
57
+ export OPENAI_API_KEY=sk-...
58
+ export GOOGLE_API_KEY=AI...
59
+ export CURSOR_API_KEY=...
60
+ ats-daemon --token abc123
61
+ ```
62
+
63
+ 2. **Via the web UI** -- keys entered in the browser are forwarded to the daemon over the WebSocket connection and injected into containers. They are never persisted on the server.
64
+
65
+ Keys from the web UI take precedence over local environment variables.
66
+
67
+ ## What It Does
68
+
69
+ The daemon is a thin orchestration layer:
70
+
71
+ - Receives test configurations (setup files, model choice, instance count) from the server
72
+ - Creates isolated Docker containers using the `agent-runner` image
73
+ - Mounts setup files into each container
74
+ - Streams container stdout/stderr back to the server line-by-line
75
+ - Reports container lifecycle events (started, stopped, crashed, OOM)
76
+ - Enforces resource limits (4GB memory, 2 CPUs, 100 PIDs per container)
77
+ - Handles graceful shutdown on SIGINT/SIGTERM
78
+
79
+ ## Security
80
+
81
+ - Containers run with enforced resource limits (memory, CPU, PIDs)
82
+ - Setup files are written to a temp directory with path traversal protection
83
+ - API keys are only held in memory, never written to disk by the daemon
84
+ - Containers use an isolated Docker network (`agent-network`)
85
+ - The daemon connects outbound to the server (no inbound ports needed)
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,37 @@
1
+ /**
2
+ * WebSocket Client - connects the daemon to the cloud server
3
+ *
4
+ * Handles:
5
+ * - Outbound connection to server (user's machine -> cloud)
6
+ * - Automatic reconnection with exponential backoff
7
+ * - Sending daemon events (container output, status changes)
8
+ * - Receiving server commands (start test, stop instance, etc.)
9
+ */
10
+ import type { ServerToDaemonMessage, DaemonToServerMessage } from './shared/protocol.js';
11
+ export interface ConnectionConfig {
12
+ /** Server URL (e.g., wss://my-server.example.com or ws://localhost:3001) */
13
+ serverUrl: string;
14
+ /** Pairing token from the web UI */
15
+ token: string;
16
+ /** Which API keys are available locally */
17
+ localApiKeys: {
18
+ anthropic: boolean;
19
+ openai: boolean;
20
+ google: boolean;
21
+ cursor: boolean;
22
+ };
23
+ }
24
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
25
+ export interface ServerConnection {
26
+ /** Current connection state */
27
+ state: ConnectionState;
28
+ /** Send a message to the server */
29
+ send: (message: DaemonToServerMessage) => void;
30
+ /** Register a handler for incoming server commands */
31
+ onCommand: (handler: (message: ServerToDaemonMessage) => void) => void;
32
+ /** Register a handler for connection state changes */
33
+ onStateChange: (handler: (state: ConnectionState) => void) => void;
34
+ /** Gracefully close the connection */
35
+ close: () => void;
36
+ }
37
+ export declare function connectToServer(config: ConnectionConfig): ServerConnection;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * WebSocket Client - connects the daemon to the cloud server
3
+ *
4
+ * Handles:
5
+ * - Outbound connection to server (user's machine -> cloud)
6
+ * - Automatic reconnection with exponential backoff
7
+ * - Sending daemon events (container output, status changes)
8
+ * - Receiving server commands (start test, stop instance, etc.)
9
+ */
10
+ import WebSocket from 'ws';
11
+ import { DAEMON_WS_PATH, PROTOCOL_VERSION } from './shared/protocol.js';
12
+ import { getDockerStatus } from './docker/checker.js';
13
+ import os from 'os';
14
+ // ---------------------------------------------------------------------------
15
+ // Connection implementation
16
+ // ---------------------------------------------------------------------------
17
+ const RECONNECT_BASE_MS = 1000;
18
+ const RECONNECT_MAX_MS = 30000;
19
+ const HEARTBEAT_INTERVAL_MS = 15000;
20
+ export function connectToServer(config) {
21
+ let ws = null;
22
+ let state = 'disconnected';
23
+ let reconnectAttempt = 0;
24
+ let reconnectTimer = null;
25
+ let heartbeatTimer = null;
26
+ let intentionalClose = false;
27
+ const startTime = Date.now();
28
+ const commandHandlers = [];
29
+ const stateHandlers = [];
30
+ function setState(newState) {
31
+ if (state === newState)
32
+ return;
33
+ state = newState;
34
+ for (const handler of stateHandlers) {
35
+ try {
36
+ handler(newState);
37
+ }
38
+ catch { /* ignore handler errors */ }
39
+ }
40
+ }
41
+ function send(message) {
42
+ if (ws && ws.readyState === WebSocket.OPEN) {
43
+ ws.send(JSON.stringify(message));
44
+ }
45
+ }
46
+ async function buildHello() {
47
+ const dockerStatus = await getDockerStatus();
48
+ return {
49
+ type: 'daemon_hello',
50
+ daemonVersion: PROTOCOL_VERSION,
51
+ docker: {
52
+ installed: dockerStatus.installed,
53
+ running: dockerStatus.running,
54
+ version: dockerStatus.version,
55
+ },
56
+ localApiKeys: config.localApiKeys,
57
+ platform: os.platform(),
58
+ arch: os.arch(),
59
+ };
60
+ }
61
+ function startHeartbeat() {
62
+ stopHeartbeat();
63
+ heartbeatTimer = setInterval(() => {
64
+ const pong = {
65
+ type: 'daemon_pong',
66
+ timestamp: Date.now(),
67
+ uptimeMs: Date.now() - startTime,
68
+ activeContainers: 0, // Will be updated by the caller
69
+ };
70
+ send(pong);
71
+ }, HEARTBEAT_INTERVAL_MS);
72
+ }
73
+ function stopHeartbeat() {
74
+ if (heartbeatTimer) {
75
+ clearInterval(heartbeatTimer);
76
+ heartbeatTimer = null;
77
+ }
78
+ }
79
+ function connect() {
80
+ if (intentionalClose)
81
+ return;
82
+ setState('connecting');
83
+ const url = `${config.serverUrl}${DAEMON_WS_PATH}?token=${encodeURIComponent(config.token)}`;
84
+ ws = new WebSocket(url);
85
+ ws.on('open', async () => {
86
+ console.log('[DAEMON] Connected to server');
87
+ reconnectAttempt = 0;
88
+ setState('connected');
89
+ // Send hello
90
+ const hello = await buildHello();
91
+ send(hello);
92
+ startHeartbeat();
93
+ });
94
+ ws.on('message', (data) => {
95
+ try {
96
+ const message = JSON.parse(data.toString());
97
+ for (const handler of commandHandlers) {
98
+ try {
99
+ handler(message);
100
+ }
101
+ catch (err) {
102
+ console.error('[DAEMON] Command handler error:', err);
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ console.error('[DAEMON] Failed to parse server message');
108
+ }
109
+ });
110
+ ws.on('close', (code, reason) => {
111
+ stopHeartbeat();
112
+ if (intentionalClose) {
113
+ setState('disconnected');
114
+ return;
115
+ }
116
+ console.log(`[DAEMON] Disconnected (code=${code}, reason=${reason?.toString() || 'none'}). Reconnecting...`);
117
+ setState('disconnected');
118
+ scheduleReconnect();
119
+ });
120
+ ws.on('error', (err) => {
121
+ console.error('[DAEMON] WebSocket error:', err.message);
122
+ setState('error');
123
+ });
124
+ }
125
+ function scheduleReconnect() {
126
+ if (intentionalClose)
127
+ return;
128
+ const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt), RECONNECT_MAX_MS);
129
+ reconnectAttempt++;
130
+ console.log(`[DAEMON] Reconnecting in ${delay}ms (attempt ${reconnectAttempt})...`);
131
+ reconnectTimer = setTimeout(connect, delay);
132
+ }
133
+ // Start initial connection
134
+ connect();
135
+ return {
136
+ get state() { return state; },
137
+ send,
138
+ onCommand(handler) { commandHandlers.push(handler); },
139
+ onStateChange(handler) { stateHandlers.push(handler); },
140
+ close() {
141
+ intentionalClose = true;
142
+ stopHeartbeat();
143
+ if (reconnectTimer)
144
+ clearTimeout(reconnectTimer);
145
+ if (ws) {
146
+ ws.close(1000, 'Daemon shutting down');
147
+ ws = null;
148
+ }
149
+ setState('disconnected');
150
+ },
151
+ };
152
+ }
153
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Docker Availability Checker (Daemon version)
3
+ *
4
+ * Checks Docker availability on the USER's local machine.
5
+ * Unlike the server version, this connects to the default Docker socket
6
+ * (/var/run/docker.sock) since the daemon runs on the user's machine
7
+ * where Docker Desktop / Docker Engine is installed.
8
+ *
9
+ * No DOCKER_HOST gating here -- the user's local Docker IS the target.
10
+ */
11
+ import Docker from 'dockerode';
12
+ export interface DockerStatus {
13
+ installed: boolean;
14
+ running: boolean;
15
+ version: string | null;
16
+ error?: string;
17
+ }
18
+ /**
19
+ * Create a dockerode instance using default connection.
20
+ * On Linux: /var/run/docker.sock
21
+ * On macOS: /var/run/docker.sock (Docker Desktop)
22
+ * On Windows: //./pipe/docker_engine (Docker Desktop)
23
+ *
24
+ * dockerode auto-detects the right socket for the platform.
25
+ */
26
+ export declare function createDockerClient(): Docker;
27
+ /**
28
+ * Check if Docker CLI is installed
29
+ */
30
+ export declare function isDockerInstalled(): Promise<boolean>;
31
+ /**
32
+ * Check if Docker daemon is running
33
+ */
34
+ export declare function isDockerRunning(): Promise<boolean>;
35
+ /**
36
+ * Get Docker version string
37
+ */
38
+ export declare function getDockerVersion(): Promise<string | null>;
39
+ /**
40
+ * Get comprehensive Docker status on the local machine
41
+ */
42
+ export declare function getDockerStatus(): Promise<DockerStatus>;
43
+ /**
44
+ * Verify Docker is available, throw if not
45
+ */
46
+ export declare function verifyDockerAvailable(): Promise<void>;
47
+ /**
48
+ * Check if the agent-runner image exists locally
49
+ */
50
+ export declare function isAgentRunnerImageAvailable(): Promise<boolean>;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Docker Availability Checker (Daemon version)
3
+ *
4
+ * Checks Docker availability on the USER's local machine.
5
+ * Unlike the server version, this connects to the default Docker socket
6
+ * (/var/run/docker.sock) since the daemon runs on the user's machine
7
+ * where Docker Desktop / Docker Engine is installed.
8
+ *
9
+ * No DOCKER_HOST gating here -- the user's local Docker IS the target.
10
+ */
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import Docker from 'dockerode';
14
+ const execAsync = promisify(exec);
15
+ const COMMAND_TIMEOUT_MS = 5000;
16
+ const DOCKER_PING_TIMEOUT_MS = 3000;
17
+ /**
18
+ * Create a dockerode instance using default connection.
19
+ * On Linux: /var/run/docker.sock
20
+ * On macOS: /var/run/docker.sock (Docker Desktop)
21
+ * On Windows: //./pipe/docker_engine (Docker Desktop)
22
+ *
23
+ * dockerode auto-detects the right socket for the platform.
24
+ */
25
+ export function createDockerClient() {
26
+ return new Docker();
27
+ }
28
+ /**
29
+ * Ping the local Docker daemon
30
+ */
31
+ async function pingDockerDaemon() {
32
+ const docker = createDockerClient();
33
+ const timeout = new Promise((_, reject) => {
34
+ setTimeout(() => reject(new Error('Docker ping timeout')), DOCKER_PING_TIMEOUT_MS);
35
+ });
36
+ try {
37
+ await Promise.race([docker.ping(), timeout]);
38
+ return { running: true };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ running: false,
43
+ error: error instanceof Error ? error.message : String(error),
44
+ };
45
+ }
46
+ }
47
+ /**
48
+ * Check if Docker CLI is installed
49
+ */
50
+ export async function isDockerInstalled() {
51
+ try {
52
+ await execAsync('docker --version', { timeout: COMMAND_TIMEOUT_MS });
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /**
60
+ * Check if Docker daemon is running
61
+ */
62
+ export async function isDockerRunning() {
63
+ const status = await pingDockerDaemon();
64
+ return status.running;
65
+ }
66
+ /**
67
+ * Get Docker version string
68
+ */
69
+ export async function getDockerVersion() {
70
+ try {
71
+ const { stdout } = await execAsync('docker --version', { timeout: COMMAND_TIMEOUT_MS });
72
+ const match = stdout.match(/Docker version ([^\s,]+)/);
73
+ if (match && match[1]) {
74
+ return match[1];
75
+ }
76
+ return null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Get comprehensive Docker status on the local machine
84
+ */
85
+ export async function getDockerStatus() {
86
+ const status = {
87
+ installed: false,
88
+ running: false,
89
+ version: null,
90
+ };
91
+ status.installed = await isDockerInstalled();
92
+ if (!status.installed) {
93
+ status.error = 'Docker is not installed. Install Docker Desktop from https://docker.com/get-started';
94
+ return status;
95
+ }
96
+ status.version = await getDockerVersion();
97
+ const daemonStatus = await pingDockerDaemon();
98
+ status.running = daemonStatus.running;
99
+ if (!status.running) {
100
+ const detail = daemonStatus.error ? ` (${daemonStatus.error})` : '';
101
+ status.error = `Docker daemon is not running. Start Docker Desktop and try again.${detail}`;
102
+ return status;
103
+ }
104
+ return status;
105
+ }
106
+ /**
107
+ * Verify Docker is available, throw if not
108
+ */
109
+ export async function verifyDockerAvailable() {
110
+ const status = await getDockerStatus();
111
+ if (!status.installed) {
112
+ throw new Error(status.error || 'Docker is not installed.');
113
+ }
114
+ if (!status.running) {
115
+ throw new Error(status.error || 'Docker daemon is not running.');
116
+ }
117
+ }
118
+ /**
119
+ * Check if the agent-runner image exists locally
120
+ */
121
+ export async function isAgentRunnerImageAvailable() {
122
+ const docker = createDockerClient();
123
+ try {
124
+ await docker.getImage('agent-runner:latest').inspect();
125
+ return true;
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ //# sourceMappingURL=checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker.js","sourceRoot":"","sources":["../../src/docker/checker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Daemon Docker module exports
3
+ */
4
+ export { createDockerClient, isDockerInstalled, isDockerRunning, getDockerVersion, getDockerStatus, verifyDockerAvailable, isAgentRunnerImageAvailable, } from './checker.js';
5
+ export type { DockerStatus } from './checker.js';
6
+ export { ContainerManager, DockerUnavailableError } from './manager.js';
7
+ export type { ContainerConfig, ContainerInfo, ContainerEvent, ContainerEventType, ContainerHealthStatus, } from './manager.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Daemon Docker module exports
3
+ */
4
+ export { createDockerClient, isDockerInstalled, isDockerRunning, getDockerVersion, getDockerStatus, verifyDockerAvailable, isAgentRunnerImageAvailable, } from './checker.js';
5
+ export { ContainerManager, DockerUnavailableError } from './manager.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/docker/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Container Manager (Daemon version)
3
+ *
4
+ * Manages Docker container lifecycle on the user's local machine.
5
+ * - Connects to local Docker (default socket)
6
+ * - Streams output back to the cloud server via a callback
7
+ * - Receives commands from the server, not directly from the UI
8
+ * - Subscribes to Docker events for OOM / crash detection
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ import type { ModelId, InstanceStatus, SetupId, ProviderMode } from '../shared/types.js';
12
+ /**
13
+ * Error thrown when Docker is not available
14
+ */
15
+ export declare class DockerUnavailableError extends Error {
16
+ constructor();
17
+ }
18
+ export interface ContainerConfig {
19
+ instanceId: string;
20
+ setupId: SetupId;
21
+ setupName: string;
22
+ model: ModelId;
23
+ providerMode: ProviderMode;
24
+ /** Absolute path to entrypoint / wrapper scripts */
25
+ scriptsPath?: string;
26
+ /** Absolute path to the directory containing setup files */
27
+ filesPath: string;
28
+ /** Absolute path for container output */
29
+ outputPath: string;
30
+ apiKeys: {
31
+ anthropic?: string;
32
+ openai?: string;
33
+ google?: string;
34
+ cursor?: string;
35
+ };
36
+ gitRemoteUrl?: string;
37
+ networkName?: string;
38
+ timeoutMs?: number;
39
+ }
40
+ export interface ContainerInfo {
41
+ instanceId: string;
42
+ setupId: SetupId;
43
+ containerId: string;
44
+ status: InstanceStatus;
45
+ startTime?: number;
46
+ endTime?: number;
47
+ exitCode?: number;
48
+ exitType?: 'complete' | 'timeout' | 'crash' | 'stopped';
49
+ }
50
+ /**
51
+ * Container health status with resource usage
52
+ * Used for periodic health check logging
53
+ */
54
+ export interface ContainerHealthStatus {
55
+ instanceId: string;
56
+ containerId: string;
57
+ status: InstanceStatus;
58
+ uptimeMs: number;
59
+ memoryUsageBytes?: number;
60
+ memoryLimitBytes?: number;
61
+ memoryPercent?: number;
62
+ cpuPercent?: number;
63
+ }
64
+ export type ContainerEventType = 'created' | 'started' | 'output' | 'commit' | 'status_change' | 'stopped' | 'error' | 'cleanup';
65
+ export interface ContainerEvent {
66
+ type: ContainerEventType;
67
+ instanceId: string;
68
+ setupId: SetupId;
69
+ containerId: string;
70
+ timestamp: number;
71
+ data?: unknown;
72
+ }
73
+ export declare class ContainerManager extends EventEmitter {
74
+ private docker;
75
+ private containers;
76
+ private containerInfo;
77
+ private timeouts;
78
+ private isListeningToEvents;
79
+ private constructor();
80
+ /**
81
+ * Factory – verifies Docker is available before returning a manager
82
+ */
83
+ static create(): Promise<ContainerManager>;
84
+ /**
85
+ * Ping Docker daemon
86
+ */
87
+ ping(): Promise<boolean>;
88
+ /**
89
+ * Create and start a container, streaming output via onOutput callback
90
+ */
91
+ runContainer(config: ContainerConfig, onOutput: (line: string) => void): Promise<ContainerInfo>;
92
+ stopContainer(instanceId: string): Promise<void>;
93
+ pauseContainer(instanceId: string): Promise<boolean>;
94
+ resumeContainer(instanceId: string): Promise<boolean>;
95
+ cleanupContainer(instanceId: string): Promise<void>;
96
+ cleanupAll(): Promise<void>;
97
+ /**
98
+ * Get resource usage stats for a running container
99
+ */
100
+ getResourceUsage(instanceId: string): Promise<{
101
+ memoryUsageBytes: number;
102
+ memoryLimitBytes: number;
103
+ memoryPercent: number;
104
+ cpuPercent: number;
105
+ } | null>;
106
+ getContainerInfo(instanceId: string): ContainerInfo | undefined;
107
+ getAllContainerInfo(): ContainerInfo[];
108
+ getActiveCount(): number;
109
+ /**
110
+ * Check if a container is paused via Docker inspect
111
+ */
112
+ isContainerPaused(instanceId: string): Promise<boolean>;
113
+ /**
114
+ * Subscribe to Docker events for OOM / crash detection
115
+ */
116
+ subscribeToDockerEvents(): Promise<void>;
117
+ /**
118
+ * Get health status for all containers including resource usage
119
+ */
120
+ getContainerHealthStatus(): Promise<ContainerHealthStatus[]>;
121
+ /**
122
+ * Parse commit line from git hook output
123
+ * Format: AGENT_COMMIT:hash:message:insertions deletions
124
+ */
125
+ static parseCommitLine(line: string): {
126
+ hash: string;
127
+ message: string;
128
+ stats: string;
129
+ };
130
+ /** Format uptime in human-readable format */
131
+ static formatUptime(uptimeMs: number): string;
132
+ /** Format bytes in human-readable format */
133
+ static formatBytes(bytes: number): string;
134
+ private monitorCompletion;
135
+ private handleTimeout;
136
+ private clearTimeout;
137
+ private emitEvent;
138
+ }