@borgee/agents-host 0.2.2 → 0.2.26
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/README.md +184 -21
- package/dist/agents-host-supervisor.d.ts +7 -5
- package/dist/agents-host-supervisor.js +24 -4
- package/dist/agents-host.d.ts +89 -15
- package/dist/agents-host.js +2099 -141
- package/dist/chat/chat-control-plane.d.ts +13 -2
- package/dist/chat/sdk-chat-control-plane.d.ts +14 -3
- package/dist/chat/sdk-chat-control-plane.js +54 -2
- package/dist/cli-args.d.ts +46 -5
- package/dist/cli-args.js +313 -32
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +112 -5
- package/dist/compatibility-gates.d.ts +35 -0
- package/dist/compatibility-gates.js +127 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +23 -5
- package/dist/connections-state-store.d.ts +81 -0
- package/dist/connections-state-store.js +228 -0
- package/dist/context/injection.d.ts +109 -0
- package/dist/context/injection.js +350 -0
- package/dist/context/prompt.d.ts +4 -1
- package/dist/context/prompt.js +170 -1
- package/dist/context/turn-preparation.d.ts +9 -0
- package/dist/context/turn-preparation.js +106 -0
- package/dist/debug.d.ts +44 -0
- package/dist/debug.js +135 -0
- package/dist/gateway/localhost-gateway.d.ts +52 -0
- package/dist/gateway/localhost-gateway.js +857 -0
- package/dist/index.js +7 -5
- package/dist/local-config.d.ts +4 -1
- package/dist/local-config.js +24 -7
- package/dist/managed-daemon-log.d.ts +34 -0
- package/dist/managed-daemon-log.js +261 -0
- package/dist/managed-daemon.d.ts +220 -0
- package/dist/managed-daemon.js +1601 -0
- package/dist/policy/authorization-audit.d.ts +63 -0
- package/dist/policy/authorization-audit.js +94 -0
- package/dist/policy/copilot-permission.d.ts +15 -0
- package/dist/policy/copilot-permission.js +193 -0
- package/dist/policy/gateway-authorization.d.ts +42 -0
- package/dist/policy/gateway-authorization.js +162 -0
- package/dist/providers/awaiting-user.d.ts +12 -0
- package/dist/providers/awaiting-user.js +151 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +8 -12
- package/dist/providers/claude/cli-client.d.ts +12 -5
- package/dist/providers/claude/cli-client.js +184 -37
- package/dist/providers/claude/session-store.d.ts +1 -0
- package/dist/providers/codex/adapter.d.ts +11 -0
- package/dist/providers/codex/adapter.js +19 -0
- package/dist/providers/codex/cli-client.d.ts +103 -0
- package/dist/providers/codex/cli-client.js +1133 -0
- package/dist/providers/codex/project-doc.d.ts +3 -0
- package/dist/providers/codex/project-doc.js +66 -0
- package/dist/providers/codex/session-store.d.ts +38 -0
- package/dist/providers/codex/session-store.js +150 -0
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +8 -12
- package/dist/providers/copilot/cli-client.d.ts +20 -2
- package/dist/providers/copilot/cli-client.js +251 -71
- package/dist/providers/copilot/session-store.d.ts +1 -0
- package/dist/providers/create-provider.d.ts +11 -2
- package/dist/providers/create-provider.js +131 -12
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -2
- package/dist/state-paths.d.ts +13 -1
- package/dist/state-paths.js +84 -3
- package/dist/task-thread-resolution.d.ts +10 -0
- package/dist/task-thread-resolution.js +48 -0
- package/dist/types.d.ts +174 -1
- package/dist/visible-mentions.d.ts +3 -0
- package/dist/visible-mentions.js +15 -0
- package/package.json +19 -17
- package/skills/borgee-agent/SKILL.md +33 -0
- package/skills/borgee-agent/borgee-agent.mjs +473 -0
- package/skills/borgee-agent/borgee-agent.py +409 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { resolveAgentsHostDebugMode } from './debug.js';
|
|
1
2
|
import { runMain } from './run.js';
|
|
2
|
-
// Plain env-var entry point (`pnpm dev` / `pnpm start`)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
3
|
+
// Plain env-var entry point (`pnpm dev` / `pnpm start`) for the standalone
|
|
4
|
+
// single-agent process. For the CLI entry point that accepts foreground
|
|
5
|
+
// `agents-host start <serverUrl> <apiKey> ...` and opt-in
|
|
6
|
+
// `agents-host start-managed <serverUrl>` / `agents-host start-managed <serverUrl> <apiKey> ...`,
|
|
7
|
+
// see cli.ts.
|
|
8
|
+
runMain({ debug: resolveAgentsHostDebugMode(false, process.env) }).catch((error) => {
|
|
7
9
|
console.error('[agents-host] fatal error:', error);
|
|
8
10
|
process.exitCode = 1;
|
|
9
11
|
});
|
package/dist/local-config.d.ts
CHANGED
|
@@ -36,16 +36,19 @@ export interface ManagedLocalConfigFileSystem extends LocalConfigFileSystem {
|
|
|
36
36
|
chmod(path: string, mode: number): Promise<void>;
|
|
37
37
|
}
|
|
38
38
|
export declare function resolveLocalConfigLayout(rootPath: string): LocalConfigLayout;
|
|
39
|
-
export declare function parseGenerateConfigSpec(value: unknown, sourceLabel: string): LocalConfigGenerateSpec;
|
|
39
|
+
export declare function parseGenerateConfigSpec(value: unknown, sourceLabel: string, env?: NodeJS.ProcessEnv): LocalConfigGenerateSpec;
|
|
40
40
|
export declare function loadLocalConfigSnapshot(hostConfigPath: string, deps?: {
|
|
41
41
|
fileSystem?: LocalConfigFileSystem;
|
|
42
42
|
acquireManagedGenerationLease?: boolean;
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
43
44
|
}): Promise<LocalConfigSnapshot>;
|
|
44
45
|
export declare function loadLocalConfigGenerateSpec(hostConfigPath: string, deps?: {
|
|
45
46
|
fileSystem?: LocalConfigFileSystem;
|
|
46
47
|
acquireManagedGenerationLease?: boolean;
|
|
48
|
+
env?: NodeJS.ProcessEnv;
|
|
47
49
|
}): Promise<LocalConfigGenerateSpec>;
|
|
48
50
|
export declare function materializeLocalConfig(rootPath: string, spec: LocalConfigGenerateSpec, deps?: {
|
|
49
51
|
fileSystem?: ManagedLocalConfigFileSystem;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
50
53
|
}): Promise<LocalConfigGenerateResult>;
|
|
51
54
|
export {};
|
package/dist/local-config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseDocument, stringify } from 'yaml';
|
|
5
|
-
import { optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
5
|
+
import { assertProviderCompatibility, optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
|
|
6
6
|
import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
|
|
7
7
|
const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
|
|
8
8
|
export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
|
|
@@ -105,6 +105,14 @@ function parseProviderCommandOverrides(value, sourceLabel) {
|
|
|
105
105
|
if (claudeArgs !== undefined) {
|
|
106
106
|
overrides.claudeArgs = claudeArgs;
|
|
107
107
|
}
|
|
108
|
+
const codexCommand = optionalNonEmptyString(value.codexCommand, 'codexCommand', sourceLabel);
|
|
109
|
+
if (codexCommand !== undefined) {
|
|
110
|
+
overrides.codexCommand = codexCommand;
|
|
111
|
+
}
|
|
112
|
+
const codexArgs = optionalStringArray(value.codexArgs, 'codexArgs', sourceLabel);
|
|
113
|
+
if (codexArgs !== undefined) {
|
|
114
|
+
overrides.codexArgs = codexArgs;
|
|
115
|
+
}
|
|
108
116
|
const copilotCommand = optionalNonEmptyString(value.copilotCommand, 'copilotCommand', sourceLabel);
|
|
109
117
|
if (copilotCommand !== undefined) {
|
|
110
118
|
overrides.copilotCommand = copilotCommand;
|
|
@@ -144,21 +152,23 @@ function parseGenerateHostConfigFile(sourceLabel, value) {
|
|
|
144
152
|
defaults: parseProviderCommandOverrides(value.defaults, `generate-config host spec ${sourceLabel}`),
|
|
145
153
|
};
|
|
146
154
|
}
|
|
147
|
-
function parseAgentConfigFile(agentConfigPath, value) {
|
|
155
|
+
function parseAgentConfigFile(agentConfigPath, value, env = process.env) {
|
|
148
156
|
const enabled = value.enabled;
|
|
149
157
|
if (enabled !== undefined && typeof enabled !== 'boolean') {
|
|
150
158
|
throw new Error(`Invalid enabled in ${agentConfigPath}: expected a boolean`);
|
|
151
159
|
}
|
|
160
|
+
const provider = resolveProvider(requireNonEmptyString(value.provider, `Invalid agent config ${agentConfigPath}: missing required provider`), `Invalid provider in ${agentConfigPath}`);
|
|
161
|
+
assertProviderCompatibility(provider, `Invalid provider in ${agentConfigPath}`, env);
|
|
152
162
|
return {
|
|
153
163
|
key: requireNonEmptyString(value.key, `Invalid agent config ${agentConfigPath}: missing required key`),
|
|
154
164
|
name: requireNonEmptyString(value.name, `Invalid agent config ${agentConfigPath}: missing required name`),
|
|
155
165
|
apiKey: requireNonEmptyString(value.apiKey, `Invalid agent config ${agentConfigPath}: missing required apiKey`),
|
|
156
|
-
provider
|
|
166
|
+
provider,
|
|
157
167
|
enabled: enabled ?? true,
|
|
158
168
|
...parseProviderCommandOverrides(value, `agent config ${agentConfigPath}`),
|
|
159
169
|
};
|
|
160
170
|
}
|
|
161
|
-
export function parseGenerateConfigSpec(value, sourceLabel) {
|
|
171
|
+
export function parseGenerateConfigSpec(value, sourceLabel, env = process.env) {
|
|
162
172
|
if (!isRecord(value)) {
|
|
163
173
|
throw new Error(`Invalid generate-config spec in ${sourceLabel}: expected a top-level object`);
|
|
164
174
|
}
|
|
@@ -175,7 +185,7 @@ export function parseGenerateConfigSpec(value, sourceLabel) {
|
|
|
175
185
|
if (!isRecord(entry)) {
|
|
176
186
|
throw new Error(`Invalid generate-config agents[${index}] in ${sourceLabel}: expected an object`);
|
|
177
187
|
}
|
|
178
|
-
const agent = parseAgentConfigFile(`generate-config agents[${index}] in ${sourceLabel}`, entry);
|
|
188
|
+
const agent = parseAgentConfigFile(`generate-config agents[${index}] in ${sourceLabel}`, entry, env);
|
|
179
189
|
const existingIndex = seenKeys.get(agent.key);
|
|
180
190
|
if (existingIndex !== undefined) {
|
|
181
191
|
throw new Error(`Duplicate generate-config agent key "${agent.key}" in ${sourceLabel} at indexes ${existingIndex} and ${index}`);
|
|
@@ -566,6 +576,12 @@ function renderAgentConfigYaml(agent) {
|
|
|
566
576
|
if (agent.claudeArgs !== undefined) {
|
|
567
577
|
config.claudeArgs = agent.claudeArgs;
|
|
568
578
|
}
|
|
579
|
+
if (agent.codexCommand !== undefined) {
|
|
580
|
+
config.codexCommand = agent.codexCommand;
|
|
581
|
+
}
|
|
582
|
+
if (agent.codexArgs !== undefined) {
|
|
583
|
+
config.codexArgs = agent.codexArgs;
|
|
584
|
+
}
|
|
569
585
|
if (agent.copilotCommand !== undefined) {
|
|
570
586
|
config.copilotCommand = agent.copilotCommand;
|
|
571
587
|
}
|
|
@@ -631,7 +647,7 @@ export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
|
|
|
631
647
|
const seenKeys = new Map();
|
|
632
648
|
for (const agentFilePath of agentFiles) {
|
|
633
649
|
const agentRecord = await loadParsedDocument(fileSystem, agentFilePath, 'agent config');
|
|
634
|
-
const agentConfig = parseAgentConfigFile(agentFilePath, agentRecord);
|
|
650
|
+
const agentConfig = parseAgentConfigFile(agentFilePath, agentRecord, deps.env);
|
|
635
651
|
const existingPath = seenKeys.get(agentConfig.key);
|
|
636
652
|
if (existingPath) {
|
|
637
653
|
throw new Error(`Duplicate agent key "${agentConfig.key}" in ${existingPath} and ${agentFilePath}`);
|
|
@@ -681,7 +697,7 @@ export async function loadLocalConfigGenerateSpec(hostConfigPath, deps = {}) {
|
|
|
681
697
|
continue;
|
|
682
698
|
}
|
|
683
699
|
const agentRecord = await loadParsedDocument(fileSystem, agentFilePath, 'agent config');
|
|
684
|
-
const agentConfig = parseAgentConfigFile(agentFilePath, agentRecord);
|
|
700
|
+
const agentConfig = parseAgentConfigFile(agentFilePath, agentRecord, deps.env);
|
|
685
701
|
const existingPath = seenKeys.get(agentConfig.key);
|
|
686
702
|
if (existingPath) {
|
|
687
703
|
throw new Error(`Duplicate agent key "${agentConfig.key}" in ${existingPath} and ${agentFilePath}`);
|
|
@@ -736,6 +752,7 @@ export async function materializeLocalConfig(rootPath, spec, deps = {}) {
|
|
|
736
752
|
await loadLocalConfigSnapshot(generationHostConfigPath, {
|
|
737
753
|
fileSystem,
|
|
738
754
|
acquireManagedGenerationLease: false,
|
|
755
|
+
env: deps.env,
|
|
739
756
|
});
|
|
740
757
|
const nextCurrentPath = join(layout.root, `.${MANAGED_CURRENT_LINK_NAME}-${generationName}`);
|
|
741
758
|
await fileSystem.symlink(join(MANAGED_GENERATIONS_DIRNAME, generationName), nextCurrentPath);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface FileStatsLike {
|
|
2
|
+
size: number;
|
|
3
|
+
ino: number | bigint;
|
|
4
|
+
isFile(): boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ManagedDaemonLogFileHandle {
|
|
7
|
+
stat(): Promise<FileStatsLike>;
|
|
8
|
+
read(buffer: Buffer, offset: number, length: number, position: number): Promise<{
|
|
9
|
+
bytesRead: number;
|
|
10
|
+
}>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
interface ManagedDaemonLogFs {
|
|
14
|
+
stat(path: string): Promise<FileStatsLike>;
|
|
15
|
+
open(path: string, flags: string): Promise<ManagedDaemonLogFileHandle>;
|
|
16
|
+
}
|
|
17
|
+
export interface RunManagedDaemonLogCommandOptions {
|
|
18
|
+
serverUrl: string;
|
|
19
|
+
lines?: number;
|
|
20
|
+
follow?: boolean;
|
|
21
|
+
pathOnly?: boolean;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
resolvedHomeDir?: string;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
export interface RunManagedDaemonLogCommandDeps {
|
|
27
|
+
fs?: ManagedDaemonLogFs;
|
|
28
|
+
write?: (chunk: string) => void;
|
|
29
|
+
sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
30
|
+
pollIntervalMs?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function resolveManagedDaemonLogCliPath(serverUrl: string, env?: NodeJS.ProcessEnv, resolvedHomeDir?: string): string;
|
|
33
|
+
export declare function runManagedDaemonLogCommand(options: RunManagedDaemonLogCommandOptions, deps?: RunManagedDaemonLogCommandDeps): Promise<void>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
import { resolveManagedDaemonLogPath, resolveManagedRuntimeRoot, } from './state-paths.js';
|
|
4
|
+
const DEFAULT_TAIL_LINES = 100;
|
|
5
|
+
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
6
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
7
|
+
const CURSOR_ANCHOR_BYTES = 256;
|
|
8
|
+
const defaultManagedDaemonLogFs = {
|
|
9
|
+
stat: (path) => fs.stat(path),
|
|
10
|
+
open: async (path, flags) => {
|
|
11
|
+
const handle = await fs.open(path, flags);
|
|
12
|
+
return wrapFileHandle(handle);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
function wrapFileHandle(handle) {
|
|
16
|
+
return {
|
|
17
|
+
stat: () => handle.stat(),
|
|
18
|
+
async read(buffer, offset, length, position) {
|
|
19
|
+
const result = await handle.read(buffer, offset, length, position);
|
|
20
|
+
return { bytesRead: result.bytesRead };
|
|
21
|
+
},
|
|
22
|
+
close: () => handle.close(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function resolveSleep(milliseconds, signal) {
|
|
26
|
+
return new Promise((resolveDelay) => {
|
|
27
|
+
if (signal?.aborted) {
|
|
28
|
+
resolveDelay();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
signal?.removeEventListener('abort', onAbort);
|
|
33
|
+
resolveDelay();
|
|
34
|
+
}, milliseconds);
|
|
35
|
+
const onAbort = () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
signal?.removeEventListener('abort', onAbort);
|
|
38
|
+
resolveDelay();
|
|
39
|
+
};
|
|
40
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function countTrailingNewlineOffset(text) {
|
|
44
|
+
let offset = text.length;
|
|
45
|
+
while (offset > 0 && text[offset - 1] === '\n') {
|
|
46
|
+
offset--;
|
|
47
|
+
}
|
|
48
|
+
return offset;
|
|
49
|
+
}
|
|
50
|
+
function sliceLastLines(text, lineCount) {
|
|
51
|
+
if (lineCount <= 0 || text.length === 0) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
const scanLimit = countTrailingNewlineOffset(text);
|
|
55
|
+
let linesSeen = 0;
|
|
56
|
+
for (let index = scanLimit - 1; index >= 0; index--) {
|
|
57
|
+
if (text[index] !== '\n') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
linesSeen++;
|
|
61
|
+
if (linesSeen === lineCount) {
|
|
62
|
+
return text.slice(index + 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
function countNewlinesInBuffer(buffer) {
|
|
68
|
+
let count = 0;
|
|
69
|
+
for (const byte of buffer) {
|
|
70
|
+
if (byte === 0x0a) {
|
|
71
|
+
count++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return count;
|
|
75
|
+
}
|
|
76
|
+
async function withOpenFile(filePath, managedFs, operation) {
|
|
77
|
+
const handle = await managedFs.open(filePath, 'r');
|
|
78
|
+
try {
|
|
79
|
+
return await operation(handle);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await handle.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function readRange(handle, start, length) {
|
|
86
|
+
const buffer = Buffer.alloc(length);
|
|
87
|
+
let totalBytesRead = 0;
|
|
88
|
+
while (totalBytesRead < length) {
|
|
89
|
+
const { bytesRead } = await handle.read(buffer, totalBytesRead, length - totalBytesRead, start + totalBytesRead);
|
|
90
|
+
if (bytesRead === 0) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
totalBytesRead += bytesRead;
|
|
94
|
+
}
|
|
95
|
+
return buffer.subarray(0, totalBytesRead);
|
|
96
|
+
}
|
|
97
|
+
async function readTailFromHandle(handle, lineCount) {
|
|
98
|
+
if (lineCount <= 0) {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
const stats = await handle.stat();
|
|
102
|
+
let position = stats.size;
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let newlinesFound = 0;
|
|
105
|
+
while (position > 0 && newlinesFound <= lineCount) {
|
|
106
|
+
const start = Math.max(0, position - READ_CHUNK_BYTES);
|
|
107
|
+
const length = position - start;
|
|
108
|
+
const chunk = await readRange(handle, start, length);
|
|
109
|
+
if (chunk.length === 0) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
chunks.unshift(Buffer.from(chunk));
|
|
113
|
+
newlinesFound += countNewlinesInBuffer(chunk);
|
|
114
|
+
position = start;
|
|
115
|
+
}
|
|
116
|
+
const collected = Buffer.concat(chunks).toString('utf8');
|
|
117
|
+
return sliceLastLines(collected, lineCount);
|
|
118
|
+
}
|
|
119
|
+
async function readCursorAnchor(handle, endPosition) {
|
|
120
|
+
if (endPosition <= 0) {
|
|
121
|
+
return Buffer.alloc(0);
|
|
122
|
+
}
|
|
123
|
+
const length = Math.min(CURSOR_ANCHOR_BYTES, endPosition);
|
|
124
|
+
const start = endPosition - length;
|
|
125
|
+
return readRange(handle, start, length);
|
|
126
|
+
}
|
|
127
|
+
function createMissingLogFileError(logPath) {
|
|
128
|
+
return new Error(`Managed daemon log file does not exist yet: ${logPath}`);
|
|
129
|
+
}
|
|
130
|
+
function createNonFileLogPathError(logPath) {
|
|
131
|
+
return new Error(`Managed daemon log path is not a regular file: ${logPath}`);
|
|
132
|
+
}
|
|
133
|
+
async function readInitialTail(logPath, lineCount, managedFs) {
|
|
134
|
+
const openErrorMessage = (error) => {
|
|
135
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
136
|
+
throw createMissingLogFileError(logPath);
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
};
|
|
140
|
+
const { text, stats, anchor } = await withOpenFile(logPath, managedFs, async (handle) => {
|
|
141
|
+
const currentStats = await handle.stat();
|
|
142
|
+
if (!currentStats.isFile()) {
|
|
143
|
+
throw createNonFileLogPathError(logPath);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
text: await readTailFromHandle(handle, lineCount),
|
|
147
|
+
stats: currentStats,
|
|
148
|
+
anchor: await readCursorAnchor(handle, currentStats.size),
|
|
149
|
+
};
|
|
150
|
+
}).catch(openErrorMessage);
|
|
151
|
+
return {
|
|
152
|
+
text,
|
|
153
|
+
cursor: {
|
|
154
|
+
ino: stats.ino,
|
|
155
|
+
position: stats.size,
|
|
156
|
+
anchor,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function readAppendedChunk(logPath, cursor, managedFs) {
|
|
161
|
+
const isNotFoundError = (error) => typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
|
162
|
+
const stats = await managedFs.stat(logPath).catch((error) => {
|
|
163
|
+
if (isNotFoundError(error)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
});
|
|
168
|
+
if (stats === null || !stats.isFile()) {
|
|
169
|
+
return {
|
|
170
|
+
bytes: Buffer.alloc(0),
|
|
171
|
+
cursor: { ino: null, position: 0, anchor: Buffer.alloc(0) },
|
|
172
|
+
reset: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
return await withOpenFile(logPath, managedFs, async (handle) => {
|
|
177
|
+
const currentStats = await handle.stat();
|
|
178
|
+
if (!currentStats.isFile()) {
|
|
179
|
+
return {
|
|
180
|
+
bytes: Buffer.alloc(0),
|
|
181
|
+
cursor: { ino: null, position: 0, anchor: Buffer.alloc(0) },
|
|
182
|
+
reset: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
let resetCursor = cursor.ino !== currentStats.ino || currentStats.size < cursor.position;
|
|
186
|
+
if (!resetCursor && cursor.anchor.length > 0) {
|
|
187
|
+
const anchor = await readCursorAnchor(handle, cursor.position);
|
|
188
|
+
if (!anchor.equals(cursor.anchor)) {
|
|
189
|
+
resetCursor = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const nextPosition = resetCursor ? 0 : cursor.position;
|
|
193
|
+
if (currentStats.size <= nextPosition) {
|
|
194
|
+
return {
|
|
195
|
+
bytes: Buffer.alloc(0),
|
|
196
|
+
cursor: { ino: currentStats.ino, position: currentStats.size, anchor: await readCursorAnchor(handle, currentStats.size) },
|
|
197
|
+
reset: resetCursor,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const bytes = await readRange(handle, nextPosition, currentStats.size - nextPosition);
|
|
201
|
+
const endPosition = nextPosition + bytes.length;
|
|
202
|
+
return {
|
|
203
|
+
bytes,
|
|
204
|
+
cursor: {
|
|
205
|
+
ino: currentStats.ino,
|
|
206
|
+
position: endPosition,
|
|
207
|
+
anchor: await readCursorAnchor(handle, endPosition),
|
|
208
|
+
},
|
|
209
|
+
reset: resetCursor,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (isNotFoundError(error)) {
|
|
215
|
+
return {
|
|
216
|
+
bytes: Buffer.alloc(0),
|
|
217
|
+
cursor: { ino: null, position: 0, anchor: Buffer.alloc(0) },
|
|
218
|
+
reset: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export function resolveManagedDaemonLogCliPath(serverUrl, env = process.env, resolvedHomeDir) {
|
|
225
|
+
const rootPath = resolveManagedRuntimeRoot(serverUrl, env, resolvedHomeDir);
|
|
226
|
+
return resolveManagedDaemonLogPath(rootPath);
|
|
227
|
+
}
|
|
228
|
+
export async function runManagedDaemonLogCommand(options, deps = {}) {
|
|
229
|
+
const lineCount = options.lines ?? DEFAULT_TAIL_LINES;
|
|
230
|
+
const follow = options.follow === true;
|
|
231
|
+
const pathOnly = options.pathOnly === true;
|
|
232
|
+
const write = deps.write ?? ((chunk) => process.stdout.write(chunk));
|
|
233
|
+
const managedFs = deps.fs ?? defaultManagedDaemonLogFs;
|
|
234
|
+
const sleep = deps.sleep ?? resolveSleep;
|
|
235
|
+
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
236
|
+
const logPath = resolveManagedDaemonLogCliPath(options.serverUrl, options.env, options.resolvedHomeDir);
|
|
237
|
+
if (pathOnly) {
|
|
238
|
+
write(`${logPath}\n`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const initial = await readInitialTail(logPath, lineCount, managedFs);
|
|
242
|
+
if (initial.text.length > 0) {
|
|
243
|
+
write(initial.text);
|
|
244
|
+
}
|
|
245
|
+
if (!follow) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let cursor = initial.cursor;
|
|
249
|
+
let decoder = new StringDecoder('utf8');
|
|
250
|
+
while (!options.signal?.aborted) {
|
|
251
|
+
const next = await readAppendedChunk(logPath, cursor, managedFs);
|
|
252
|
+
cursor = next.cursor;
|
|
253
|
+
if (next.reset) {
|
|
254
|
+
decoder = new StringDecoder('utf8');
|
|
255
|
+
}
|
|
256
|
+
if (next.bytes.length > 0) {
|
|
257
|
+
write(decoder.write(next.bytes));
|
|
258
|
+
}
|
|
259
|
+
await sleep(pollIntervalMs, options.signal);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { ChildProcess } from 'node:child_process';
|
|
3
|
+
import type { LocalAgentConfigFile, LocalConfigGenerateSpec } from './types.js';
|
|
4
|
+
export declare const MANAGED_DAEMON_ERROR_CODES: readonly ["MANAGED_BINDING_MISMATCH", "MANAGED_RUNTIME_NOT_PRESENT", "MANAGED_SPEC_INVALID", "MANAGED_BOOTSTRAP_LOCK_TIMEOUT", "MANAGED_DAEMON_START_FAILED", "MANAGED_DAEMON_NOT_READY", "MANAGED_RELOAD_FAILED", "MANAGED_CONVERGENCE_FAILED"];
|
|
5
|
+
export type ManagedDaemonErrorCode = (typeof MANAGED_DAEMON_ERROR_CODES)[number];
|
|
6
|
+
export interface ManagedDaemonErrorPayload {
|
|
7
|
+
code: ManagedDaemonErrorCode;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BootstrapManagedDaemonStartOptions {
|
|
11
|
+
serverUrl: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ManagedDaemonStatusResponse {
|
|
18
|
+
ok: true;
|
|
19
|
+
type: 'status';
|
|
20
|
+
borgeeBaseUrl: string;
|
|
21
|
+
hostConfigPath: string;
|
|
22
|
+
logPath?: string;
|
|
23
|
+
agentCount: number;
|
|
24
|
+
enabledAgentCount: number;
|
|
25
|
+
managedRuntimeFingerprint?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ManagedDaemonUpsertAgentResponse {
|
|
28
|
+
ok: true;
|
|
29
|
+
type: 'upsertAgent';
|
|
30
|
+
agentKey: string;
|
|
31
|
+
hostConfigPath: string;
|
|
32
|
+
logPath?: string;
|
|
33
|
+
agentCount: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ManagedDaemonDescribeSpecResponse {
|
|
36
|
+
ok: true;
|
|
37
|
+
type: 'describeSpec';
|
|
38
|
+
present: boolean;
|
|
39
|
+
spec?: LocalConfigGenerateSpec;
|
|
40
|
+
}
|
|
41
|
+
export interface ManagedDaemonShutdownResponse {
|
|
42
|
+
ok: true;
|
|
43
|
+
type: 'shutdown';
|
|
44
|
+
}
|
|
45
|
+
export interface ManagedDaemonApplySpecResponse {
|
|
46
|
+
ok: true;
|
|
47
|
+
type: 'applySpec';
|
|
48
|
+
hostConfigPath: string;
|
|
49
|
+
logPath?: string;
|
|
50
|
+
agentCount: number;
|
|
51
|
+
enabledAgentCount: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ManagedDaemonErrorResponse {
|
|
54
|
+
ok: false;
|
|
55
|
+
error: ManagedDaemonErrorPayload;
|
|
56
|
+
}
|
|
57
|
+
export type ManagedDaemonControlResponse = ManagedDaemonStatusResponse | ManagedDaemonUpsertAgentResponse | ManagedDaemonDescribeSpecResponse | ManagedDaemonShutdownResponse | ManagedDaemonApplySpecResponse | ManagedDaemonErrorResponse;
|
|
58
|
+
export type ManagedDaemonControlRequest = {
|
|
59
|
+
type: 'status';
|
|
60
|
+
} | {
|
|
61
|
+
type: 'describeSpec';
|
|
62
|
+
borgeeBaseUrl: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'upsertAgent';
|
|
65
|
+
borgeeBaseUrl: string;
|
|
66
|
+
agent: LocalAgentConfigFile;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'shutdown';
|
|
69
|
+
} | {
|
|
70
|
+
type: 'applySpec';
|
|
71
|
+
borgeeBaseUrl: string;
|
|
72
|
+
spec: LocalConfigGenerateSpec;
|
|
73
|
+
};
|
|
74
|
+
export interface BootstrapManagedDaemonStartDeps {
|
|
75
|
+
loadSpec?: (configPath: string) => Promise<LocalConfigGenerateSpec>;
|
|
76
|
+
materialize?: (rootPath: string, spec: LocalConfigGenerateSpec) => Promise<unknown>;
|
|
77
|
+
sendRequest?: (rootPath: string, request: ManagedDaemonControlRequest) => Promise<ManagedDaemonControlResponse>;
|
|
78
|
+
spawnDaemon?: (options: {
|
|
79
|
+
rootPath: string;
|
|
80
|
+
debug: boolean;
|
|
81
|
+
}) => ChildProcess | Promise<ChildProcess>;
|
|
82
|
+
waitForDaemonReady?: (rootPath: string, sendRequest: NonNullable<BootstrapManagedDaemonStartDeps['sendRequest']>, child: ChildProcess) => Promise<void>;
|
|
83
|
+
acquireBootstrapLock?: (rootPath: string) => Promise<() => Promise<void>>;
|
|
84
|
+
waitForShutdown?: (rootPath: string) => Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
export interface ManagedSupervisorControl {
|
|
87
|
+
start(): Promise<void>;
|
|
88
|
+
stop(): Promise<void>;
|
|
89
|
+
reloadNow(failOnStartError?: boolean): Promise<void>;
|
|
90
|
+
hasActiveAgent(key: string): boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface ManagedAgentsHostDaemonDeps {
|
|
93
|
+
createSupervisor?: (configPath: string, debug: boolean) => ManagedSupervisorControl;
|
|
94
|
+
loadSpec?: (configPath: string) => Promise<LocalConfigGenerateSpec>;
|
|
95
|
+
materialize?: (rootPath: string, spec: LocalConfigGenerateSpec) => Promise<unknown>;
|
|
96
|
+
logger?: Pick<Console, 'log' | 'error'>;
|
|
97
|
+
logPath?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface DescribeManagedSpecOptions {
|
|
100
|
+
serverUrl: string;
|
|
101
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
102
|
+
}
|
|
103
|
+
export interface CleanupManagedRuntimeOptions {
|
|
104
|
+
serverUrl: string;
|
|
105
|
+
purge?: boolean;
|
|
106
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
107
|
+
}
|
|
108
|
+
export interface CleanupManagedRuntimeDeps {
|
|
109
|
+
sendRequest?: (rootPath: string, request: ManagedDaemonControlRequest) => Promise<ManagedDaemonControlResponse>;
|
|
110
|
+
removeRuntimeRoot?: (rootPath: string) => Promise<void>;
|
|
111
|
+
canConnectToSocket?: (socketPath: string) => Promise<boolean>;
|
|
112
|
+
waitForShutdown?: (rootPath: string) => Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
export interface DescribeManagedSpecDeps {
|
|
115
|
+
loadSpec?: (configPath: string) => Promise<LocalConfigGenerateSpec>;
|
|
116
|
+
sendRequest?: (rootPath: string, request: ManagedDaemonControlRequest) => Promise<ManagedDaemonControlResponse>;
|
|
117
|
+
}
|
|
118
|
+
export type DescribeManagedSpecResult = {
|
|
119
|
+
ok: true;
|
|
120
|
+
present: false;
|
|
121
|
+
} | {
|
|
122
|
+
ok: true;
|
|
123
|
+
present: true;
|
|
124
|
+
spec: LocalConfigGenerateSpec;
|
|
125
|
+
} | ManagedDaemonErrorResponse;
|
|
126
|
+
export interface ApplyManagedSpecOptions {
|
|
127
|
+
serverUrl: string;
|
|
128
|
+
spec: unknown;
|
|
129
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
130
|
+
debug?: boolean;
|
|
131
|
+
}
|
|
132
|
+
export interface ApplyManagedSpecDeps {
|
|
133
|
+
loadSpec?: (configPath: string) => Promise<LocalConfigGenerateSpec>;
|
|
134
|
+
materialize?: (rootPath: string, spec: LocalConfigGenerateSpec) => Promise<unknown>;
|
|
135
|
+
sendRequest?: (rootPath: string, request: ManagedDaemonControlRequest) => Promise<ManagedDaemonControlResponse>;
|
|
136
|
+
spawnDaemon?: (options: {
|
|
137
|
+
rootPath: string;
|
|
138
|
+
debug: boolean;
|
|
139
|
+
}) => ChildProcess | Promise<ChildProcess>;
|
|
140
|
+
waitForDaemonReady?: (rootPath: string, sendRequest: NonNullable<ApplyManagedSpecDeps['sendRequest']>, child: ChildProcess) => Promise<void>;
|
|
141
|
+
acquireBootstrapLock?: (rootPath: string) => Promise<() => Promise<void>>;
|
|
142
|
+
createSupervisor?: (configPath: string, debug: boolean) => ManagedSupervisorControl;
|
|
143
|
+
clearManagedRuntime?: (rootPath: string) => Promise<void>;
|
|
144
|
+
waitForShutdown?: (rootPath: string) => Promise<void>;
|
|
145
|
+
}
|
|
146
|
+
export type ApplyManagedSpecResult = {
|
|
147
|
+
ok: true;
|
|
148
|
+
daemon: 'started' | 'reused';
|
|
149
|
+
agentCount: number;
|
|
150
|
+
enabledAgentCount: number;
|
|
151
|
+
logPath?: string;
|
|
152
|
+
} | ManagedDaemonErrorResponse;
|
|
153
|
+
export interface ManagedDaemonLogFileHandle {
|
|
154
|
+
logPath: string;
|
|
155
|
+
fd: number;
|
|
156
|
+
close(): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
interface SpawnDetachedManagedProcessOptions {
|
|
159
|
+
rootPath: string;
|
|
160
|
+
command: string;
|
|
161
|
+
args: string[];
|
|
162
|
+
env?: NodeJS.ProcessEnv;
|
|
163
|
+
}
|
|
164
|
+
interface SpawnDetachedManagedProcessDeps {
|
|
165
|
+
openLogFile?: (rootPath: string) => Promise<ManagedDaemonLogFileHandle>;
|
|
166
|
+
spawnProcess?: typeof spawn;
|
|
167
|
+
}
|
|
168
|
+
interface SpawnManagedDaemonProcessDeps {
|
|
169
|
+
spawnDetachedProcess?: typeof spawnDetachedManagedProcess;
|
|
170
|
+
}
|
|
171
|
+
export declare function resolveManagedAgentKey(apiKey: string): string;
|
|
172
|
+
export declare function buildManagedLocalAgentConfig(options: BootstrapManagedDaemonStartOptions): {
|
|
173
|
+
rootPath: string;
|
|
174
|
+
hostConfigPath: string;
|
|
175
|
+
borgeeBaseUrl: string;
|
|
176
|
+
agent: LocalAgentConfigFile;
|
|
177
|
+
};
|
|
178
|
+
export declare function acquireBootstrapLock(rootPath: string): Promise<() => Promise<void>>;
|
|
179
|
+
export declare function sendManagedDaemonRequest(rootPath: string, request: ManagedDaemonControlRequest): Promise<ManagedDaemonControlResponse>;
|
|
180
|
+
export declare function waitForManagedDaemonReady(rootPath: string, sendRequest: (rootPath: string, request: ManagedDaemonControlRequest) => Promise<ManagedDaemonControlResponse>, child: ChildProcess): Promise<void>;
|
|
181
|
+
export declare function cleanupManagedRuntime(options: CleanupManagedRuntimeOptions, deps?: CleanupManagedRuntimeDeps): Promise<void>;
|
|
182
|
+
export declare function openManagedDaemonLogFile(rootPath: string): Promise<ManagedDaemonLogFileHandle>;
|
|
183
|
+
export declare function spawnDetachedManagedProcess(options: SpawnDetachedManagedProcessOptions, deps?: SpawnDetachedManagedProcessDeps): Promise<ChildProcess>;
|
|
184
|
+
export declare function spawnManagedDaemonProcess(options: {
|
|
185
|
+
rootPath: string;
|
|
186
|
+
debug: boolean;
|
|
187
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
188
|
+
}, deps?: SpawnManagedDaemonProcessDeps): Promise<ChildProcess>;
|
|
189
|
+
export declare function bootstrapManagedDaemonStart(options: BootstrapManagedDaemonStartOptions, deps?: BootstrapManagedDaemonStartDeps): Promise<void>;
|
|
190
|
+
export declare function describeManagedSpec(options: DescribeManagedSpecOptions, deps?: DescribeManagedSpecDeps): Promise<DescribeManagedSpecResult>;
|
|
191
|
+
export declare function applyManagedSpec(options: ApplyManagedSpecOptions, deps?: ApplyManagedSpecDeps): Promise<ApplyManagedSpecResult>;
|
|
192
|
+
export declare class ManagedAgentsHostDaemon {
|
|
193
|
+
private readonly rootPath;
|
|
194
|
+
private readonly hostConfigPath;
|
|
195
|
+
private readonly socketPath;
|
|
196
|
+
private readonly createSupervisor;
|
|
197
|
+
private readonly supervisor;
|
|
198
|
+
private readonly loadSpec;
|
|
199
|
+
private readonly materialize;
|
|
200
|
+
private readonly managedRuntimeFingerprint;
|
|
201
|
+
private readonly logger;
|
|
202
|
+
private readonly logPath;
|
|
203
|
+
private readonly server;
|
|
204
|
+
private requestQueue;
|
|
205
|
+
private started;
|
|
206
|
+
private ready;
|
|
207
|
+
private ownsSocket;
|
|
208
|
+
private shutdownRequested;
|
|
209
|
+
private ownedSocketInode;
|
|
210
|
+
constructor(rootPath: string, debug?: boolean, deps?: ManagedAgentsHostDaemonDeps);
|
|
211
|
+
start(): Promise<void>;
|
|
212
|
+
stop(): Promise<void>;
|
|
213
|
+
private listen;
|
|
214
|
+
private handleSocket;
|
|
215
|
+
private respond;
|
|
216
|
+
private handleRequest;
|
|
217
|
+
private enqueue;
|
|
218
|
+
private publishSpecWithRollback;
|
|
219
|
+
}
|
|
220
|
+
export {};
|