@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { HostLogger, summarizeError } from './debug.js';
|
|
5
|
+
import { resolveConnectionsStatePath } from './state-paths.js';
|
|
6
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
7
|
+
const PRIVATE_STATE_FILE_MODE = 0o600;
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
function loadDatabaseSync() {
|
|
10
|
+
const sqliteModule = require('node:sqlite');
|
|
11
|
+
if (typeof sqliteModule.DatabaseSync !== 'function') {
|
|
12
|
+
throw new Error('node:sqlite DatabaseSync is unavailable');
|
|
13
|
+
}
|
|
14
|
+
return sqliteModule.DatabaseSync;
|
|
15
|
+
}
|
|
16
|
+
export class SqliteConnectionsStateStore {
|
|
17
|
+
options;
|
|
18
|
+
database = null;
|
|
19
|
+
closed = false;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
loadProviderSessions(provider, agentId) {
|
|
24
|
+
const rows = this.ensureDatabase().prepare(`SELECT channel_id, session_id
|
|
25
|
+
FROM provider_channel_sessions
|
|
26
|
+
WHERE provider = ? AND agent_id = ?
|
|
27
|
+
ORDER BY channel_id ASC`).all(provider, agentId);
|
|
28
|
+
return Object.fromEntries(rows.map((row) => [row.channel_id, row.session_id]));
|
|
29
|
+
}
|
|
30
|
+
saveProviderSessions(provider, agentId, sessions) {
|
|
31
|
+
const database = this.ensureDatabase();
|
|
32
|
+
const entries = Object.entries(sessions).sort(([left], [right]) => left.localeCompare(right));
|
|
33
|
+
database.exec('BEGIN IMMEDIATE');
|
|
34
|
+
try {
|
|
35
|
+
database.prepare(`DELETE FROM provider_channel_sessions
|
|
36
|
+
WHERE provider = ? AND agent_id = ?`).run(provider, agentId);
|
|
37
|
+
if (entries.length > 0) {
|
|
38
|
+
const insert = database.prepare(`INSERT INTO provider_channel_sessions (provider, agent_id, channel_id, session_id)
|
|
39
|
+
VALUES (?, ?, ?, ?)`);
|
|
40
|
+
for (const [channelId, sessionId] of entries) {
|
|
41
|
+
insert.run(provider, agentId, channelId, sessionId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
database.exec('COMMIT');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
database.exec('ROLLBACK');
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
hasTokenBinding(token) {
|
|
52
|
+
const row = this.ensureDatabase().prepare(`SELECT 1
|
|
53
|
+
FROM channel_token_bindings
|
|
54
|
+
WHERE token = ?
|
|
55
|
+
LIMIT 1`).get(token);
|
|
56
|
+
return row != null;
|
|
57
|
+
}
|
|
58
|
+
loadTokenBinding(token) {
|
|
59
|
+
const row = this.ensureDatabase().prepare(`SELECT token, agent_id, channel_id
|
|
60
|
+
FROM channel_token_bindings
|
|
61
|
+
WHERE token = ? AND revoked_at IS NULL`).get(token);
|
|
62
|
+
if (!row) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
token: row.token,
|
|
67
|
+
agentId: row.agent_id,
|
|
68
|
+
channelId: row.channel_id,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
loadTokenBindingForChannel(agentId, channelId) {
|
|
72
|
+
const row = this.ensureDatabase().prepare(`SELECT token, agent_id, channel_id
|
|
73
|
+
FROM channel_token_bindings
|
|
74
|
+
WHERE agent_id = ? AND channel_id = ? AND revoked_at IS NULL
|
|
75
|
+
ORDER BY rowid DESC
|
|
76
|
+
LIMIT 1`).get(agentId, channelId);
|
|
77
|
+
if (!row) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
token: row.token,
|
|
82
|
+
agentId: row.agent_id,
|
|
83
|
+
channelId: row.channel_id,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
replaceTokenBinding(binding) {
|
|
87
|
+
const database = this.ensureDatabase();
|
|
88
|
+
database.exec('BEGIN IMMEDIATE');
|
|
89
|
+
try {
|
|
90
|
+
const bindingByToken = this.loadStoredTokenBinding(binding.token);
|
|
91
|
+
if (bindingByToken) {
|
|
92
|
+
if (bindingByToken.revokedAt != null) {
|
|
93
|
+
throw new Error(`channel token ${binding.token} was revoked and cannot be reused`);
|
|
94
|
+
}
|
|
95
|
+
if (bindingByToken.agentId !== binding.agentId || bindingByToken.channelId !== binding.channelId) {
|
|
96
|
+
throw new Error(`channel token is already bound to ${bindingByToken.agentId}/${bindingByToken.channelId}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
database.prepare(`UPDATE channel_token_bindings
|
|
100
|
+
SET revoked_at = unixepoch()
|
|
101
|
+
WHERE agent_id = ? AND channel_id = ? AND token <> ? AND revoked_at IS NULL`).run(binding.agentId, binding.channelId, binding.token);
|
|
102
|
+
database.prepare(`INSERT INTO channel_token_bindings (token, agent_id, channel_id, revoked_at)
|
|
103
|
+
VALUES (?, ?, ?, NULL)
|
|
104
|
+
ON CONFLICT(token) DO UPDATE SET
|
|
105
|
+
agent_id = excluded.agent_id,
|
|
106
|
+
channel_id = excluded.channel_id,
|
|
107
|
+
revoked_at = NULL`).run(binding.token, binding.agentId, binding.channelId);
|
|
108
|
+
database.exec('COMMIT');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
database.exec('ROLLBACK');
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
deleteTokenBinding(token) {
|
|
116
|
+
this.ensureDatabase().prepare(`DELETE FROM channel_token_bindings
|
|
117
|
+
WHERE token = ?`).run(token);
|
|
118
|
+
}
|
|
119
|
+
revokeTokenBinding(token) {
|
|
120
|
+
this.ensureDatabase().prepare(`UPDATE channel_token_bindings
|
|
121
|
+
SET revoked_at = unixepoch()
|
|
122
|
+
WHERE token = ? AND revoked_at IS NULL`).run(token);
|
|
123
|
+
}
|
|
124
|
+
revokeAllTokenBindings() {
|
|
125
|
+
this.ensureDatabase().exec('UPDATE channel_token_bindings SET revoked_at = unixepoch() WHERE revoked_at IS NULL');
|
|
126
|
+
}
|
|
127
|
+
close() {
|
|
128
|
+
this.database?.close();
|
|
129
|
+
this.database = null;
|
|
130
|
+
this.closed = true;
|
|
131
|
+
}
|
|
132
|
+
ensureDatabase() {
|
|
133
|
+
if (this.closed) {
|
|
134
|
+
throw new Error('connections state store is closed');
|
|
135
|
+
}
|
|
136
|
+
if (this.database) {
|
|
137
|
+
return this.database;
|
|
138
|
+
}
|
|
139
|
+
const path = (this.options.resolveDatabasePath ?? resolveConnectionsStatePath)(this.options.stateRootDir);
|
|
140
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
141
|
+
const database = (this.options.openDatabase ?? ((databasePath) => new (loadDatabaseSync())(databasePath)))(path);
|
|
142
|
+
try {
|
|
143
|
+
chmodSync(path, PRIVATE_STATE_FILE_MODE);
|
|
144
|
+
database.exec(`PRAGMA busy_timeout = ${this.options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
|
|
145
|
+
database.exec('PRAGMA journal_mode = DELETE');
|
|
146
|
+
database.exec(`CREATE TABLE IF NOT EXISTS provider_channel_sessions (
|
|
147
|
+
provider TEXT NOT NULL,
|
|
148
|
+
agent_id TEXT NOT NULL,
|
|
149
|
+
channel_id TEXT NOT NULL,
|
|
150
|
+
session_id TEXT NOT NULL,
|
|
151
|
+
PRIMARY KEY (provider, agent_id, channel_id)
|
|
152
|
+
)`);
|
|
153
|
+
database.exec(`CREATE TABLE IF NOT EXISTS channel_token_bindings (
|
|
154
|
+
token TEXT PRIMARY KEY,
|
|
155
|
+
agent_id TEXT NOT NULL,
|
|
156
|
+
channel_id TEXT NOT NULL,
|
|
157
|
+
revoked_at INTEGER
|
|
158
|
+
)`);
|
|
159
|
+
const columns = database.prepare(`SELECT name
|
|
160
|
+
FROM pragma_table_info('channel_token_bindings')
|
|
161
|
+
WHERE name = 'revoked_at'
|
|
162
|
+
LIMIT 1`).all();
|
|
163
|
+
if (columns.length === 0) {
|
|
164
|
+
database.exec('ALTER TABLE channel_token_bindings ADD COLUMN revoked_at INTEGER');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
database.close();
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
this.database = database;
|
|
172
|
+
return database;
|
|
173
|
+
}
|
|
174
|
+
loadStoredTokenBinding(token) {
|
|
175
|
+
const row = this.ensureDatabase().prepare(`SELECT token, agent_id, channel_id, revoked_at
|
|
176
|
+
FROM channel_token_bindings
|
|
177
|
+
WHERE token = ?`).get(token);
|
|
178
|
+
if (!row) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
token: row.token,
|
|
183
|
+
agentId: row.agent_id,
|
|
184
|
+
channelId: row.channel_id,
|
|
185
|
+
revokedAt: row.revoked_at,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export class ConnectionsStateBackedChannelSessionStore {
|
|
190
|
+
options;
|
|
191
|
+
logger;
|
|
192
|
+
constructor(options) {
|
|
193
|
+
this.options = options;
|
|
194
|
+
this.logger = options.logger ?? new HostLogger();
|
|
195
|
+
}
|
|
196
|
+
async load(agentId) {
|
|
197
|
+
return this.options.legacyStore.load(agentId);
|
|
198
|
+
}
|
|
199
|
+
async save(agentId, sessions) {
|
|
200
|
+
await this.options.legacyStore.save(agentId, sessions);
|
|
201
|
+
if (!this.options.sqliteStateStore) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
this.options.sqliteStateStore.saveProviderSessions(this.options.provider, agentId, sessions);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
this.logger.error(`failed to mirror ${this.options.provider} session map into connections state`, {
|
|
209
|
+
provider: this.options.provider,
|
|
210
|
+
error: summarizeError(error),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
close() {
|
|
215
|
+
this.options.sqliteStateStore?.close();
|
|
216
|
+
this.options.legacyStore.close?.();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function createProviderConnectionsSessionStore(options) {
|
|
220
|
+
return new ConnectionsStateBackedChannelSessionStore({
|
|
221
|
+
provider: options.provider,
|
|
222
|
+
legacyStore: options.legacyStore,
|
|
223
|
+
sqliteStateStore: options.gateEnabled
|
|
224
|
+
? new SqliteConnectionsStateStore({ stateRootDir: options.stateRootDir })
|
|
225
|
+
: undefined,
|
|
226
|
+
logger: options.logger,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { LocalhostGatewayBootstrapMetadata, ProviderCollaborationContext, SkillRuntimeBootstrapMetadata, TaskAssignmentThreadContext, TaskWorkspaceContext } from '../types.js';
|
|
2
|
+
export interface SkillRuntimeBootstrapPayload {
|
|
3
|
+
skillDirectoryPath: string;
|
|
4
|
+
nodeCliPath: string;
|
|
5
|
+
pythonCliPath: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ChannelContextPayload {
|
|
8
|
+
schemaVersion: 1;
|
|
9
|
+
channelId: string;
|
|
10
|
+
skillRuntime?: SkillRuntimeBootstrapPayload;
|
|
11
|
+
localhostGateway?: LocalhostGatewayBootstrapMetadata;
|
|
12
|
+
taskAssignmentContext?: TaskAssignmentThreadContext;
|
|
13
|
+
taskWorkspace?: TaskWorkspaceContext;
|
|
14
|
+
}
|
|
15
|
+
export interface PreparedChannelContext {
|
|
16
|
+
directoryPath: string;
|
|
17
|
+
payload: ChannelContextPayload;
|
|
18
|
+
payloadPath?: string;
|
|
19
|
+
gatewayAuthPath?: string;
|
|
20
|
+
skillRuntime?: SkillRuntimeBootstrapMetadata;
|
|
21
|
+
localhostGateway?: LocalhostGatewayBootstrapMetadata;
|
|
22
|
+
taskWorkspace?: TaskWorkspaceContext;
|
|
23
|
+
}
|
|
24
|
+
export declare class ChannelContextPreparationError extends Error {
|
|
25
|
+
readonly partialContext: PreparedChannelContext;
|
|
26
|
+
constructor(message: string, partialContext: PreparedChannelContext, options?: ErrorOptions);
|
|
27
|
+
}
|
|
28
|
+
export interface ChannelContextStore {
|
|
29
|
+
prepare(input: {
|
|
30
|
+
channelId: string;
|
|
31
|
+
collaboration?: ProviderCollaborationContext;
|
|
32
|
+
incomingMessageType?: string;
|
|
33
|
+
incomingContent?: string;
|
|
34
|
+
}): Promise<PreparedChannelContext>;
|
|
35
|
+
prepare(channelId: string, options?: {
|
|
36
|
+
collaboration?: ProviderCollaborationContext;
|
|
37
|
+
incomingMessageType?: string;
|
|
38
|
+
incomingContent?: string;
|
|
39
|
+
}): Promise<PreparedChannelContext>;
|
|
40
|
+
}
|
|
41
|
+
export interface SkillAssetResolver {
|
|
42
|
+
resolve(): Promise<SkillRuntimeBootstrapMetadata>;
|
|
43
|
+
}
|
|
44
|
+
export interface LocalhostGatewayContextPublisher {
|
|
45
|
+
resolveBootstrap(channelId: string, options?: {
|
|
46
|
+
collaboration?: {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
turnExecutionId: string;
|
|
49
|
+
};
|
|
50
|
+
}): LocalhostGatewayBootstrapMetadata & {
|
|
51
|
+
token: string;
|
|
52
|
+
turnExecutionId?: string;
|
|
53
|
+
};
|
|
54
|
+
publishPayload(channelId: string, payloadPath: string, payload: ChannelContextPayload): void;
|
|
55
|
+
clearChannel(channelId: string): void;
|
|
56
|
+
}
|
|
57
|
+
interface ContextFileSystem {
|
|
58
|
+
mkdir(path: string, options?: {
|
|
59
|
+
mode?: number;
|
|
60
|
+
recursive?: boolean;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
readdir(path: string): Promise<string[]>;
|
|
63
|
+
readFile(path: string, options: {
|
|
64
|
+
encoding: BufferEncoding;
|
|
65
|
+
} | BufferEncoding): Promise<string>;
|
|
66
|
+
writeFile(path: string, data: string, options?: {
|
|
67
|
+
encoding?: BufferEncoding;
|
|
68
|
+
mode?: number;
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
unlink(path: string): Promise<void>;
|
|
71
|
+
access(path: string): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
interface FileChannelContextStoreOptions {
|
|
74
|
+
fileSystem?: ContextFileSystem;
|
|
75
|
+
skillRuntimeEnabled?: boolean;
|
|
76
|
+
skillAssetResolver?: SkillAssetResolver;
|
|
77
|
+
localhostGateway?: LocalhostGatewayContextPublisher;
|
|
78
|
+
taskWorkspaceRootDir?: string;
|
|
79
|
+
}
|
|
80
|
+
export declare function extractTaskIdFromTaskAssignmentContent(incomingContent: string | undefined): string | undefined;
|
|
81
|
+
export declare function encodeChannelPathSegment(channelId: string): string;
|
|
82
|
+
export declare function resolveChannelContextDirectory(stateRootDir: string, channelId: string): string;
|
|
83
|
+
export declare function resolveChannelContextPayloadPath(stateRootDir: string, channelId: string): string;
|
|
84
|
+
export declare function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir: string): string;
|
|
85
|
+
export declare function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir: string, channelId: string, taskId: string): string;
|
|
86
|
+
export declare function resolveChannelContextGatewayAuthPath(stateRootDir: string, channelId: string, turnExecutionId?: string): string;
|
|
87
|
+
export declare function resolveGatewayAuthPathFromPayloadPath(payloadPath: string): string;
|
|
88
|
+
export declare function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl: string, fileSystem?: Pick<ContextFileSystem, 'access'>): Promise<SkillRuntimeBootstrapMetadata>;
|
|
89
|
+
export declare class FileChannelContextStore implements ChannelContextStore {
|
|
90
|
+
private readonly stateRootDir;
|
|
91
|
+
private readonly fileSystem;
|
|
92
|
+
private readonly skillRuntimeEnabled;
|
|
93
|
+
private readonly skillAssetResolver;
|
|
94
|
+
private readonly localhostGateway?;
|
|
95
|
+
private readonly startupWorkspaceRootDir;
|
|
96
|
+
constructor(stateRootDir: string, options?: FileChannelContextStoreOptions);
|
|
97
|
+
prepare(inputOrChannelId: {
|
|
98
|
+
channelId: string;
|
|
99
|
+
collaboration?: ProviderCollaborationContext;
|
|
100
|
+
incomingMessageType?: string;
|
|
101
|
+
incomingContent?: string;
|
|
102
|
+
} | string, options?: {
|
|
103
|
+
collaboration?: ProviderCollaborationContext;
|
|
104
|
+
incomingMessageType?: string;
|
|
105
|
+
incomingContent?: string;
|
|
106
|
+
}): Promise<PreparedChannelContext>;
|
|
107
|
+
private resolveSkillRuntimeBestEffort;
|
|
108
|
+
}
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
|
|
5
|
+
const CHANNEL_CONTEXT_PAYLOAD_FILENAME = 'context.json';
|
|
6
|
+
const CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME = '.localhost-gateway-auth.json';
|
|
7
|
+
const TASK_WORKSPACE_ROOT_DIRNAME = '.borgee-task-workspaces';
|
|
8
|
+
export class ChannelContextPreparationError extends Error {
|
|
9
|
+
partialContext;
|
|
10
|
+
constructor(message, partialContext, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.partialContext = partialContext;
|
|
13
|
+
this.name = 'ChannelContextPreparationError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const DEFAULT_FILE_SYSTEM = {
|
|
17
|
+
async mkdir(path, options) {
|
|
18
|
+
await fs.mkdir(path, options);
|
|
19
|
+
},
|
|
20
|
+
async readdir(path) {
|
|
21
|
+
return await fs.readdir(path);
|
|
22
|
+
},
|
|
23
|
+
async readFile(path, options) {
|
|
24
|
+
return await fs.readFile(path, options);
|
|
25
|
+
},
|
|
26
|
+
async writeFile(path, data, options) {
|
|
27
|
+
await fs.writeFile(path, data, options);
|
|
28
|
+
},
|
|
29
|
+
async unlink(path) {
|
|
30
|
+
await fs.unlink(path);
|
|
31
|
+
},
|
|
32
|
+
async access(path) {
|
|
33
|
+
await fs.access(path);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const BORGEE_AGENT_SKILL_DIRNAME = 'borgee-agent';
|
|
37
|
+
const TASK_ASSIGNMENT_PREAMBLE_PATTERN = /^This is a task assignment \(task_id: (.+)\)\. The work belongs to this thread\b/u;
|
|
38
|
+
function toSkillRuntimePayload(skillRuntime) {
|
|
39
|
+
if (!skillRuntime) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
skillDirectoryPath: skillRuntime.skillDirectoryPath,
|
|
44
|
+
nodeCliPath: skillRuntime.nodeCliPath,
|
|
45
|
+
pythonCliPath: skillRuntime.pythonCliPath,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
|
|
49
|
+
if (!localhostGateway) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
channelId,
|
|
55
|
+
localhostGateway: {
|
|
56
|
+
token: localhostGateway.token,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function buildChannelContextPayload(channelId, skillRuntime, localhostGateway, options) {
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
channelId,
|
|
64
|
+
...(skillRuntime ? { skillRuntime: toSkillRuntimePayload(skillRuntime) } : {}),
|
|
65
|
+
...(localhostGateway ? { localhostGateway } : {}),
|
|
66
|
+
...(options?.taskAssignmentContext ? { taskAssignmentContext: options.taskAssignmentContext } : {}),
|
|
67
|
+
...(options?.taskWorkspace ? { taskWorkspace: options.taskWorkspace } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function isRecord(value) {
|
|
71
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
function canRoundTripThroughUriComponentEncoding(value) {
|
|
74
|
+
try {
|
|
75
|
+
return decodeURIComponent(encodeURIComponent(value)) === value;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof URIError) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isUsableTaskId(value) {
|
|
85
|
+
return value.length > 0
|
|
86
|
+
&& !/[\u0000-\u001f\u007f\s]/u.test(value)
|
|
87
|
+
&& canRoundTripThroughUriComponentEncoding(value);
|
|
88
|
+
}
|
|
89
|
+
function sanitizeTaskAssignmentContext(value) {
|
|
90
|
+
if (!isRecord(value) || value.active !== true) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const currentTaskId = typeof value.currentTaskId === 'string'
|
|
94
|
+
? value.currentTaskId.trim()
|
|
95
|
+
: '';
|
|
96
|
+
return currentTaskId && isUsableTaskId(currentTaskId)
|
|
97
|
+
? { active: true, currentTaskId }
|
|
98
|
+
: { active: true };
|
|
99
|
+
}
|
|
100
|
+
export function extractTaskIdFromTaskAssignmentContent(incomingContent) {
|
|
101
|
+
if (typeof incomingContent !== 'string') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const currentTaskId = incomingContent.match(TASK_ASSIGNMENT_PREAMBLE_PATTERN)?.[1]?.trim();
|
|
105
|
+
return currentTaskId && isUsableTaskId(currentTaskId)
|
|
106
|
+
? currentTaskId
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
function buildTaskAssignmentContextForTurn(options, existingContext) {
|
|
110
|
+
if (options?.incomingMessageType === 'task_assignment') {
|
|
111
|
+
const currentTaskId = extractTaskIdFromTaskAssignmentContent(options.incomingContent);
|
|
112
|
+
return currentTaskId ? { active: true, currentTaskId } : { active: true };
|
|
113
|
+
}
|
|
114
|
+
return existingContext;
|
|
115
|
+
}
|
|
116
|
+
async function readExistingTaskAssignmentContext(payloadPath, fileSystem) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = await fileSystem.readFile(payloadPath, { encoding: 'utf8' });
|
|
119
|
+
const payload = JSON.parse(raw);
|
|
120
|
+
return sanitizeTaskAssignmentContext(payload.taskAssignmentContext);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export function encodeChannelPathSegment(channelId) {
|
|
127
|
+
const encoded = Buffer.from(channelId, 'utf8').toString('hex');
|
|
128
|
+
return encoded.length > 0 ? encoded : 'empty';
|
|
129
|
+
}
|
|
130
|
+
export function resolveChannelContextDirectory(stateRootDir, channelId) {
|
|
131
|
+
return resolve(stateRootDir, CHANNEL_CONTEXT_ROOT_DIRNAME, encodeChannelPathSegment(channelId));
|
|
132
|
+
}
|
|
133
|
+
export function resolveChannelContextPayloadPath(stateRootDir, channelId) {
|
|
134
|
+
return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_CONTEXT_PAYLOAD_FILENAME);
|
|
135
|
+
}
|
|
136
|
+
export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
|
|
137
|
+
return join(resolve(startupWorkspaceRootDir), TASK_WORKSPACE_ROOT_DIRNAME);
|
|
138
|
+
}
|
|
139
|
+
export function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir, channelId, taskId) {
|
|
140
|
+
return join(resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir), encodeChannelPathSegment(channelId), encodeChannelPathSegment(taskId));
|
|
141
|
+
}
|
|
142
|
+
export function resolveChannelContextGatewayAuthPath(stateRootDir, channelId, turnExecutionId) {
|
|
143
|
+
const filename = turnExecutionId
|
|
144
|
+
? `.localhost-gateway-auth.${Buffer.from(turnExecutionId, 'utf8').toString('hex')}.json`
|
|
145
|
+
: CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME;
|
|
146
|
+
return join(resolveChannelContextDirectory(stateRootDir, channelId), filename);
|
|
147
|
+
}
|
|
148
|
+
export function resolveGatewayAuthPathFromPayloadPath(payloadPath) {
|
|
149
|
+
return join(dirname(payloadPath), CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME);
|
|
150
|
+
}
|
|
151
|
+
const LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX = '.localhost-gateway-auth.';
|
|
152
|
+
async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
|
|
153
|
+
let entries;
|
|
154
|
+
try {
|
|
155
|
+
entries = await fileSystem.readdir(directoryPath);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const code = typeof error === 'object' && error && 'code' in error ? error.code : undefined;
|
|
159
|
+
if (code === 'ENOENT') {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
await Promise.all(entries
|
|
165
|
+
.filter((entry) => entry.startsWith(LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX)
|
|
166
|
+
&& entry.endsWith('.json'))
|
|
167
|
+
.map(async (entry) => {
|
|
168
|
+
await fileSystem.unlink(join(directoryPath, entry)).catch((error) => {
|
|
169
|
+
if (error.code !== 'ENOENT') {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
async function resolvePackageRootFromModuleUrl(moduleUrl, fileSystem = DEFAULT_FILE_SYSTEM) {
|
|
176
|
+
let currentPath = dirname(fileURLToPath(moduleUrl));
|
|
177
|
+
for (;;) {
|
|
178
|
+
try {
|
|
179
|
+
await fileSystem.access(join(currentPath, 'package.json'));
|
|
180
|
+
return currentPath;
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
const parentPath = resolve(currentPath, '..');
|
|
184
|
+
if (parentPath === currentPath) {
|
|
185
|
+
throw new Error(`Unable to resolve agents-host package root from ${moduleUrl}`);
|
|
186
|
+
}
|
|
187
|
+
currentPath = parentPath;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl, fileSystem = DEFAULT_FILE_SYSTEM) {
|
|
191
|
+
const packageRootPath = await resolvePackageRootFromModuleUrl(moduleUrl, fileSystem);
|
|
192
|
+
const skillDirectoryPath = join(packageRootPath, 'skills', BORGEE_AGENT_SKILL_DIRNAME);
|
|
193
|
+
const skillMarkdownPath = join(skillDirectoryPath, 'SKILL.md');
|
|
194
|
+
const nodeCliPath = join(skillDirectoryPath, 'borgee-agent.mjs');
|
|
195
|
+
const pythonCliPath = join(skillDirectoryPath, 'borgee-agent.py');
|
|
196
|
+
await Promise.all([
|
|
197
|
+
fileSystem.access(skillDirectoryPath),
|
|
198
|
+
fileSystem.access(skillMarkdownPath),
|
|
199
|
+
fileSystem.access(nodeCliPath),
|
|
200
|
+
fileSystem.access(pythonCliPath),
|
|
201
|
+
]);
|
|
202
|
+
return {
|
|
203
|
+
skillDirectoryPath,
|
|
204
|
+
skillMarkdownPath,
|
|
205
|
+
nodeCliPath,
|
|
206
|
+
pythonCliPath,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
class PackageSkillAssetResolver {
|
|
210
|
+
moduleUrl;
|
|
211
|
+
fileSystem;
|
|
212
|
+
resolutionPromise = null;
|
|
213
|
+
constructor(moduleUrl, fileSystem = DEFAULT_FILE_SYSTEM) {
|
|
214
|
+
this.moduleUrl = moduleUrl;
|
|
215
|
+
this.fileSystem = fileSystem;
|
|
216
|
+
}
|
|
217
|
+
resolve() {
|
|
218
|
+
this.resolutionPromise ??= resolveBorgeeAgentSkillRuntimeAssets(this.moduleUrl, this.fileSystem);
|
|
219
|
+
return this.resolutionPromise;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export class FileChannelContextStore {
|
|
223
|
+
stateRootDir;
|
|
224
|
+
fileSystem;
|
|
225
|
+
skillRuntimeEnabled;
|
|
226
|
+
skillAssetResolver;
|
|
227
|
+
localhostGateway;
|
|
228
|
+
startupWorkspaceRootDir;
|
|
229
|
+
constructor(stateRootDir, options = {}) {
|
|
230
|
+
this.stateRootDir = stateRootDir;
|
|
231
|
+
this.fileSystem = options.fileSystem ?? DEFAULT_FILE_SYSTEM;
|
|
232
|
+
this.skillRuntimeEnabled = options.skillRuntimeEnabled ?? false;
|
|
233
|
+
this.skillAssetResolver = options.skillAssetResolver ?? new PackageSkillAssetResolver(import.meta.url, this.fileSystem);
|
|
234
|
+
this.localhostGateway = options.localhostGateway;
|
|
235
|
+
this.startupWorkspaceRootDir = resolve(options.taskWorkspaceRootDir ?? process.cwd());
|
|
236
|
+
}
|
|
237
|
+
async prepare(inputOrChannelId, options) {
|
|
238
|
+
const input = typeof inputOrChannelId === 'string'
|
|
239
|
+
? { channelId: inputOrChannelId, ...options }
|
|
240
|
+
: inputOrChannelId;
|
|
241
|
+
const turnMode = input.collaboration?.turnMode ?? 'ordinary';
|
|
242
|
+
const collaborationCommandsEnabled = input.collaboration?.enabled === true && input.collaboration.sendRoutesAllowed === true;
|
|
243
|
+
const collaborationRoutesAllowedForTurnMode = turnMode === 'ordinary';
|
|
244
|
+
const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
|
|
245
|
+
const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
|
|
246
|
+
const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
|
|
247
|
+
const gatewayAuthPath = resolveGatewayAuthPathFromPayloadPath(payloadPath);
|
|
248
|
+
const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
|
|
249
|
+
const skillRuntime = this.skillRuntimeEnabled
|
|
250
|
+
? await this.resolveSkillRuntimeBestEffort()
|
|
251
|
+
: undefined;
|
|
252
|
+
const issuedLocalhostGateway = skillRuntime
|
|
253
|
+
? this.localhostGateway?.resolveBootstrap(input.channelId, {
|
|
254
|
+
collaboration: auxiliaryCollaborationEnabled && input.collaboration?.turnExecutionId
|
|
255
|
+
? {
|
|
256
|
+
enabled: true,
|
|
257
|
+
turnExecutionId: input.collaboration.turnExecutionId,
|
|
258
|
+
}
|
|
259
|
+
: undefined,
|
|
260
|
+
})
|
|
261
|
+
: undefined;
|
|
262
|
+
const localhostGateway = issuedLocalhostGateway
|
|
263
|
+
? {
|
|
264
|
+
baseUrl: issuedLocalhostGateway.baseUrl,
|
|
265
|
+
...(auxiliaryCollaborationEnabled
|
|
266
|
+
? {
|
|
267
|
+
collaboration: {
|
|
268
|
+
enabled: true,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
: {}),
|
|
272
|
+
}
|
|
273
|
+
: undefined;
|
|
274
|
+
const taskAssignmentContext = buildTaskAssignmentContextForTurn({
|
|
275
|
+
incomingMessageType: input.incomingMessageType,
|
|
276
|
+
incomingContent: input.incomingContent,
|
|
277
|
+
}, existingTaskAssignmentContext);
|
|
278
|
+
const taskWorkspace = taskAssignmentContext?.currentTaskId
|
|
279
|
+
? {
|
|
280
|
+
currentTaskId: taskAssignmentContext.currentTaskId,
|
|
281
|
+
rootPath: resolveTaskWorkspaceDirectory(this.startupWorkspaceRootDir, input.channelId, taskAssignmentContext.currentTaskId),
|
|
282
|
+
}
|
|
283
|
+
: undefined;
|
|
284
|
+
const payload = buildChannelContextPayload(input.channelId, skillRuntime, localhostGateway, taskAssignmentContext || taskWorkspace
|
|
285
|
+
? {
|
|
286
|
+
...(taskAssignmentContext ? { taskAssignmentContext } : {}),
|
|
287
|
+
...(taskWorkspace ? { taskWorkspace } : {}),
|
|
288
|
+
}
|
|
289
|
+
: undefined);
|
|
290
|
+
const gatewayAuthPayload = buildLocalhostGatewayAuthPayload(input.channelId, issuedLocalhostGateway);
|
|
291
|
+
let taskWorkspaceMaterialized = false;
|
|
292
|
+
let payloadWritten = false;
|
|
293
|
+
let gatewayAuthWritten = false;
|
|
294
|
+
const preparedContext = {
|
|
295
|
+
directoryPath,
|
|
296
|
+
payload,
|
|
297
|
+
payloadPath,
|
|
298
|
+
gatewayAuthPath: gatewayAuthPayload ? gatewayAuthPath : undefined,
|
|
299
|
+
skillRuntime,
|
|
300
|
+
localhostGateway,
|
|
301
|
+
taskWorkspace,
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
304
|
+
await this.fileSystem.mkdir(directoryPath, { recursive: true, mode: 0o700 });
|
|
305
|
+
if (taskWorkspace) {
|
|
306
|
+
await this.fileSystem.mkdir(taskWorkspace.rootPath, { recursive: true, mode: 0o700 });
|
|
307
|
+
taskWorkspaceMaterialized = true;
|
|
308
|
+
}
|
|
309
|
+
await pruneLegacyGatewayAuthSidecars(this.fileSystem, directoryPath);
|
|
310
|
+
await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
311
|
+
payloadWritten = true;
|
|
312
|
+
if (gatewayAuthPayload) {
|
|
313
|
+
await this.fileSystem.writeFile(gatewayAuthPath, `${JSON.stringify(gatewayAuthPayload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
314
|
+
gatewayAuthWritten = true;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
await this.fileSystem.unlink(gatewayAuthPath).catch((error) => {
|
|
318
|
+
if (error.code !== 'ENOENT') {
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
this.localhostGateway?.clearChannel(input.channelId);
|
|
323
|
+
}
|
|
324
|
+
if (issuedLocalhostGateway) {
|
|
325
|
+
this.localhostGateway?.publishPayload(input.channelId, payloadPath, payload);
|
|
326
|
+
}
|
|
327
|
+
return preparedContext;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const partialContext = {
|
|
331
|
+
directoryPath,
|
|
332
|
+
payload,
|
|
333
|
+
payloadPath: payloadWritten ? payloadPath : undefined,
|
|
334
|
+
gatewayAuthPath: gatewayAuthWritten ? gatewayAuthPath : undefined,
|
|
335
|
+
skillRuntime: payloadWritten ? skillRuntime : undefined,
|
|
336
|
+
localhostGateway: payloadWritten ? localhostGateway : undefined,
|
|
337
|
+
taskWorkspace: taskWorkspaceMaterialized ? taskWorkspace : undefined,
|
|
338
|
+
};
|
|
339
|
+
throw new ChannelContextPreparationError('failed to persist channel context payload', partialContext, { cause: error });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async resolveSkillRuntimeBestEffort() {
|
|
343
|
+
try {
|
|
344
|
+
return await this.skillAssetResolver.resolve();
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|