@borgee/agents-host 0.2.97 → 0.2.101

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.
Files changed (35) hide show
  1. package/dist/agents-host.d.ts +8 -0
  2. package/dist/agents-host.js +258 -64
  3. package/dist/chat/chat-control-plane.d.ts +13 -3
  4. package/dist/chat/sdk-chat-control-plane.d.ts +30 -5
  5. package/dist/chat/sdk-chat-control-plane.js +131 -3
  6. package/dist/context/skill-manual.js +1 -1
  7. package/dist/execution-telemetry.d.ts +96 -0
  8. package/dist/execution-telemetry.js +583 -0
  9. package/dist/gateway/channel-file-workspace.d.ts +15 -0
  10. package/dist/gateway/channel-file-workspace.js +130 -0
  11. package/dist/gateway/localhost-gateway.js +140 -0
  12. package/dist/plugin-sdk.js +72 -3
  13. package/dist/plugin-sdk.js.map +2 -2
  14. package/dist/policy/gateway-authorization.d.ts +1 -1
  15. package/dist/policy/gateway-authorization.js +22 -2
  16. package/dist/providers/claude/adapter.js +1 -0
  17. package/dist/providers/claude/cli-client.js +4 -0
  18. package/dist/providers/codex/adapter.js +1 -0
  19. package/dist/providers/codex/cli-client.js +5 -0
  20. package/dist/providers/copilot/adapter.js +4 -0
  21. package/dist/providers/copilot/cli-client.js +11 -1
  22. package/dist/providers/copilot/sdk-session.d.ts +12 -3
  23. package/dist/providers/copilot/sdk-session.js +222 -5
  24. package/dist/providers/create-provider.js +4 -0
  25. package/dist/providers/prompt-usage.d.ts +8 -0
  26. package/dist/providers/prompt-usage.js +52 -0
  27. package/dist/state-paths.d.ts +2 -0
  28. package/dist/state-paths.js +6 -0
  29. package/dist/types.d.ts +41 -0
  30. package/dist/typing-lease.d.ts +14 -0
  31. package/dist/typing-lease.js +31 -0
  32. package/package.json +2 -2
  33. package/skills/borgee-agent/SKILL.md +12 -4
  34. package/skills/borgee-agent/scripts/borgee-agent.mjs +89 -0
  35. package/skills/borgee-agent/scripts/borgee-agent.py +90 -0
@@ -0,0 +1,130 @@
1
+ import { constants as fsConstants, promises as fs } from 'node:fs';
2
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
+ export const CHANNEL_FILE_WORKING_COPY_DIRECTORY = '.borgee/channel-files';
4
+ export const CHANNEL_FILE_TRANSFER_MAX_BYTES = 10 * 1024 * 1024;
5
+ export class ChannelFileWorkspaceError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = 'ChannelFileWorkspaceError';
11
+ }
12
+ }
13
+ export function validatePortableRelativePath(path) {
14
+ if (path.length === 0
15
+ || isAbsolute(path)
16
+ || path.includes('\\')
17
+ || path.includes('\0')
18
+ || path.split('/').some((part) => part === '' || part === '.' || part === '..' || /[\u0000-\u001f\u007f]/u.test(part))
19
+ || /^[A-Za-z]:/u.test(path)) {
20
+ throw new ChannelFileWorkspaceError('invalid_file_path', `Invalid relative file path: ${path}`);
21
+ }
22
+ return path;
23
+ }
24
+ export function requireExecutionRoot(payload) {
25
+ const workspace = payload.resolvedWorkspace;
26
+ if (workspace?.authority !== 'task-execution-target' || !isAbsolute(workspace.rootPath)) {
27
+ throw new ChannelFileWorkspaceError('execution_local_directory_required', 'Channel file transfer requires a task execution.local_directory');
28
+ }
29
+ return workspace.rootPath;
30
+ }
31
+ export async function readWorkspaceFile(rootPath, sourcePath) {
32
+ const safePath = validatePortableRelativePath(sourcePath);
33
+ const rootRealPath = await fs.realpath(rootPath);
34
+ const candidate = resolve(rootRealPath, ...safePath.split('/'));
35
+ assertContained(rootRealPath, candidate);
36
+ await rejectSymlinks(rootRealPath, candidate);
37
+ let stat;
38
+ try {
39
+ stat = await fs.stat(candidate);
40
+ }
41
+ catch (error) {
42
+ if (error.code === 'ENOENT') {
43
+ throw new ChannelFileWorkspaceError('file_not_found', `Workspace file not found: ${sourcePath}`);
44
+ }
45
+ throw error;
46
+ }
47
+ if (!stat.isFile()) {
48
+ throw new ChannelFileWorkspaceError('file_not_found', `Workspace path is not a file: ${sourcePath}`);
49
+ }
50
+ if (stat.size > CHANNEL_FILE_TRANSFER_MAX_BYTES) {
51
+ throw new ChannelFileWorkspaceError('file_too_large', 'File too large (max 10MB)');
52
+ }
53
+ return await fs.readFile(candidate);
54
+ }
55
+ export async function writeWorkingCopy(rootPath, channelPath, content, overwrite) {
56
+ if (content.byteLength > CHANNEL_FILE_TRANSFER_MAX_BYTES) {
57
+ throw new ChannelFileWorkspaceError('file_too_large', 'File too large (max 10MB)');
58
+ }
59
+ const safePath = validatePortableRelativePath(channelPath);
60
+ const executionRoot = await fs.realpath(rootPath);
61
+ const workingRoot = resolve(executionRoot, ...CHANNEL_FILE_WORKING_COPY_DIRECTORY.split('/'));
62
+ assertContained(executionRoot, workingRoot);
63
+ await ensureSafeDirectory(executionRoot, workingRoot);
64
+ const destination = resolve(workingRoot, ...safePath.split('/'));
65
+ assertContained(workingRoot, destination);
66
+ await ensureSafeDirectory(workingRoot, dirname(destination));
67
+ await rejectSymlinks(workingRoot, destination, true);
68
+ try {
69
+ await fs.writeFile(destination, content, { flag: overwrite ? 'w' : 'wx', mode: 0o600 });
70
+ }
71
+ catch (error) {
72
+ if (error.code === 'EEXIST') {
73
+ throw new ChannelFileWorkspaceError('file_exists', `Working copy already exists: ${channelPath}`);
74
+ }
75
+ throw error;
76
+ }
77
+ return destination;
78
+ }
79
+ async function ensureSafeDirectory(rootPath, directoryPath) {
80
+ const rel = relative(rootPath, directoryPath);
81
+ if (rel === '') {
82
+ return;
83
+ }
84
+ let current = rootPath;
85
+ for (const segment of rel.split(sep)) {
86
+ current = resolve(current, segment);
87
+ try {
88
+ const stat = await fs.lstat(current);
89
+ if (stat.isSymbolicLink()) {
90
+ throw new ChannelFileWorkspaceError('symlink_not_allowed', `Symbolic link is not allowed: ${current}`);
91
+ }
92
+ if (!stat.isDirectory()) {
93
+ throw new ChannelFileWorkspaceError('invalid_file_path', `Path component is not a directory: ${current}`);
94
+ }
95
+ }
96
+ catch (error) {
97
+ if (error.code !== 'ENOENT') {
98
+ throw error;
99
+ }
100
+ await fs.mkdir(current, { mode: 0o700 });
101
+ }
102
+ }
103
+ }
104
+ async function rejectSymlinks(rootPath, candidate, allowMissingLeaf = false) {
105
+ const rel = relative(rootPath, candidate);
106
+ let current = rootPath;
107
+ const parts = rel.split(sep);
108
+ for (const [index, segment] of parts.entries()) {
109
+ current = resolve(current, segment);
110
+ try {
111
+ const stat = await fs.lstat(current);
112
+ if (stat.isSymbolicLink()) {
113
+ throw new ChannelFileWorkspaceError('symlink_not_allowed', `Symbolic link is not allowed: ${current}`);
114
+ }
115
+ }
116
+ catch (error) {
117
+ if (error.code === 'ENOENT' && allowMissingLeaf && index === parts.length - 1) {
118
+ return;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ await fs.access(candidate, fsConstants.F_OK);
124
+ }
125
+ function assertContained(rootPath, candidate) {
126
+ const rel = relative(rootPath, candidate);
127
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
128
+ throw new ChannelFileWorkspaceError('invalid_file_path', 'Path escapes the execution directory');
129
+ }
130
+ }
@@ -7,6 +7,7 @@ import { evaluateGatewayAuthorization, } from '../policy/gateway-authorization.j
7
7
  import { resolveConnectionsStatePath } from '../state-paths.js';
8
8
  import { findTaskForThread } from '../task-thread-resolution.js';
9
9
  import { extractVisibleMentionIds } from '../visible-mentions.js';
10
+ import { ChannelFileWorkspaceError, readWorkspaceFile, requireExecutionRoot, validatePortableRelativePath, writeWorkingCopy, } from './channel-file-workspace.js';
10
11
  const LOOPBACK_HOST = '127.0.0.1';
11
12
  export const LOCALHOST_GATEWAY_HISTORY_LIMIT = 20;
12
13
  const LOCALHOST_GATEWAY_BODY_LIMIT_BYTES = 32 * 1024;
@@ -422,6 +423,89 @@ class LoopbackLocalhostGatewayController {
422
423
  this.recordAudit('authorized', 200, decision.path, request.method ?? 'GET', decision.binding);
423
424
  return;
424
425
  }
426
+ case 'files': {
427
+ const files = await this.controlPlane.listChannelFiles({ channelId: binding.channelId });
428
+ this.sendJson(response, 200, { files });
429
+ this.recordAudit('authorized', 200, decision.path, request.method ?? 'GET', decision.binding);
430
+ return;
431
+ }
432
+ case 'file-content': {
433
+ const url = new URL(request.url ?? '/', baseUrl);
434
+ const path = readRequiredQueryString(url, 'path');
435
+ if (!path) {
436
+ throw new GatewayHttpError(400, { error: 'missing_file_path' }, 'bad-request');
437
+ }
438
+ validatePortableRelativePath(path);
439
+ const content = await this.controlPlane.readChannelFile({
440
+ channelId: binding.channelId,
441
+ path,
442
+ textOnly: true,
443
+ });
444
+ this.sendJson(response, 200, content);
445
+ this.recordAudit('authorized', 200, decision.path, request.method ?? 'GET', decision.binding);
446
+ return;
447
+ }
448
+ case 'file-download': {
449
+ const body = parseFileTransferBody(await readGatewayJsonBody(request));
450
+ const rootPath = requireExecutionRoot(binding.payload ?? {});
451
+ const content = await this.controlPlane.readChannelFile({
452
+ channelId: binding.channelId,
453
+ path: body.path,
454
+ textOnly: false,
455
+ });
456
+ if (content.base64 == null) {
457
+ throw new GatewayHttpError(502, { error: 'invalid_file_content' }, 'invalid-file-content');
458
+ }
459
+ const localPath = await writeWorkingCopy(rootPath, body.path, Buffer.from(content.base64, 'base64'), body.overwrite);
460
+ this.sendJson(response, 200, { path: body.path, localPath, sizeBytes: content.sizeBytes });
461
+ this.recordAudit('authorized', 200, decision.path, request.method ?? 'POST', decision.binding);
462
+ return;
463
+ }
464
+ case 'file-sync': {
465
+ const body = parseFileSyncBody(await readGatewayJsonBody(request));
466
+ const rootPath = requireExecutionRoot(binding.payload ?? {});
467
+ const entries = (await this.controlPlane.listChannelFiles({ channelId: binding.channelId }))
468
+ .filter((file) => !file.isDirectory)
469
+ .filter((file) => body.path == null || file.path === body.path || file.path.startsWith(`${body.path}/`));
470
+ const synced = [];
471
+ for (const file of entries) {
472
+ const content = await this.controlPlane.readChannelFile({
473
+ channelId: binding.channelId,
474
+ path: file.path,
475
+ textOnly: false,
476
+ });
477
+ if (content.base64 == null) {
478
+ throw new GatewayHttpError(502, { error: 'invalid_file_content' }, 'invalid-file-content');
479
+ }
480
+ synced.push({
481
+ path: file.path,
482
+ localPath: await writeWorkingCopy(rootPath, file.path, Buffer.from(content.base64, 'base64'), body.overwrite),
483
+ sizeBytes: content.sizeBytes,
484
+ });
485
+ }
486
+ this.sendJson(response, 200, { synced });
487
+ this.recordAudit('authorized', 200, decision.path, request.method ?? 'POST', decision.binding);
488
+ return;
489
+ }
490
+ case 'file-publish': {
491
+ const body = parseFilePublishBody(await readGatewayJsonBody(request));
492
+ const rootPath = requireExecutionRoot(binding.payload ?? {});
493
+ const workspace = binding.payload?.resolvedWorkspace;
494
+ if (workspace?.authority !== 'task-execution-target' || !binding.agentId) {
495
+ throw new GatewayHttpError(403, { error: 'publish_context_required' }, 'publish-context-required');
496
+ }
497
+ const content = await readWorkspaceFile(rootPath, body.sourcePath);
498
+ const file = await this.controlPlane.publishChannelFile({
499
+ channelId: binding.channelId,
500
+ path: body.channelPath,
501
+ contentType: body.contentType ?? inferContentType(body.sourcePath),
502
+ base64: content.toString('base64'),
503
+ overwrite: body.overwrite,
504
+ });
505
+ this.sendJson(response, 200, { file });
506
+ this.recordAudit('authorized', 200, decision.path, request.method ?? 'POST', decision.binding);
507
+ return;
508
+ }
425
509
  case 'draft': {
426
510
  if (!binding.payload?.localhostGateway?.collaboration?.enabled) {
427
511
  this.sendJson(response, 404, { error: 'not_found' });
@@ -599,6 +683,20 @@ class LoopbackLocalhostGatewayController {
599
683
  }
600
684
  }
601
685
  catch (error) {
686
+ if (error instanceof ChannelFileWorkspaceError) {
687
+ const statusCode = error.code === 'file_not_found'
688
+ ? 404
689
+ : error.code === 'file_exists'
690
+ ? 409
691
+ : error.code === 'file_too_large'
692
+ ? 413
693
+ : error.code === 'execution_local_directory_required'
694
+ ? 403
695
+ : 400;
696
+ this.sendJson(response, statusCode, { error: error.code });
697
+ this.recordAudit(error.code, statusCode, decision.path, request.method ?? 'GET', decision.binding);
698
+ return;
699
+ }
602
700
  const gatewayError = error instanceof GatewayHttpError
603
701
  ? error
604
702
  : mapGatewayControlPlaneError(error, 'authorized');
@@ -757,6 +855,48 @@ async function readGatewayJsonBody(request) {
757
855
  throw new GatewayHttpError(400, { error: 'invalid_json' }, 'invalid-json');
758
856
  }
759
857
  }
858
+ function parseFileTransferBody(value) {
859
+ if (!isRecord(value) || typeof value.path !== 'string') {
860
+ throw new GatewayHttpError(400, { error: 'invalid_file_request' }, 'bad-request');
861
+ }
862
+ return { path: validatePortableRelativePath(value.path), overwrite: value.overwrite === true };
863
+ }
864
+ function parseFileSyncBody(value) {
865
+ if (!isRecord(value) || (value.path != null && typeof value.path !== 'string')) {
866
+ throw new GatewayHttpError(400, { error: 'invalid_file_request' }, 'bad-request');
867
+ }
868
+ return {
869
+ ...(typeof value.path === 'string' ? { path: validatePortableRelativePath(value.path) } : {}),
870
+ overwrite: value.overwrite === true,
871
+ };
872
+ }
873
+ function parseFilePublishBody(value) {
874
+ if (!isRecord(value)
875
+ || typeof value.sourcePath !== 'string'
876
+ || typeof value.channelPath !== 'string'
877
+ || (value.contentType != null && typeof value.contentType !== 'string')) {
878
+ throw new GatewayHttpError(400, { error: 'invalid_file_request' }, 'bad-request');
879
+ }
880
+ return {
881
+ sourcePath: validatePortableRelativePath(value.sourcePath),
882
+ channelPath: validatePortableRelativePath(value.channelPath),
883
+ ...(typeof value.contentType === 'string' ? { contentType: value.contentType } : {}),
884
+ overwrite: value.overwrite === true,
885
+ };
886
+ }
887
+ function inferContentType(path) {
888
+ const extension = path.toLowerCase().split('.').at(-1);
889
+ const byExtension = {
890
+ css: 'text/css', csv: 'text/csv', gif: 'image/gif', html: 'text/html',
891
+ jpeg: 'image/jpeg', jpg: 'image/jpeg', js: 'text/javascript',
892
+ json: 'application/json', md: 'text/markdown', pdf: 'application/pdf',
893
+ png: 'image/png', py: 'text/x-python', toml: 'application/toml',
894
+ ts: 'text/typescript', txt: 'text/plain', webp: 'image/webp',
895
+ xml: 'application/xml', yaml: 'application/yaml', yml: 'application/yaml',
896
+ zip: 'application/zip',
897
+ };
898
+ return (extension && byExtension[extension]) || 'application/octet-stream';
899
+ }
760
900
  async function readCollaborationRequestBody(request) {
761
901
  const bodyText = await readRequestBody(request, LOCALHOST_GATEWAY_COLLABORATION_BODY_LIMIT_BYTES);
762
902
  if (!bodyText.ok) {
@@ -4717,6 +4717,18 @@ function encodeActionPayload(action) {
4717
4717
  after: action.after,
4718
4718
  limit: action.limit
4719
4719
  };
4720
+ case "list_channel_files":
4721
+ return { channel_id: action.channelId };
4722
+ case "read_channel_file":
4723
+ return { channel_id: action.channelId, path: action.path, text_only: action.textOnly };
4724
+ case "publish_channel_file":
4725
+ return {
4726
+ channel_id: action.channelId,
4727
+ path: action.path,
4728
+ content_type: action.contentType,
4729
+ base64: action.base64,
4730
+ overwrite: action.overwrite ?? false
4731
+ };
4720
4732
  case "list_users":
4721
4733
  case "list_channels":
4722
4734
  return {};
@@ -4823,6 +4835,53 @@ function decodeActionResult(op, payloadJSON, zoneInput) {
4823
4835
  editedAt: m.edited_at != null ? Number(m.edited_at) : void 0
4824
4836
  }));
4825
4837
  }
4838
+ case "list_channel_files": {
4839
+ const arr = Array.isArray(p) ? p : [];
4840
+ return arr.map((file) => ({
4841
+ id: String(file.id ?? ""),
4842
+ ownerUserId: String(file.owner_user_id ?? ""),
4843
+ channelId: String(file.channel_id ?? ""),
4844
+ path: String(file.path ?? ""),
4845
+ parentPath: String(file.parent_path ?? ""),
4846
+ name: String(file.name ?? ""),
4847
+ isDirectory: Boolean(file.is_directory),
4848
+ mimeType: String(file.mime_type ?? ""),
4849
+ sizeBytes: Number(file.size_bytes ?? 0),
4850
+ source: String(file.source ?? ""),
4851
+ sourceMessageId: String(file.source_message_id ?? ""),
4852
+ createdAt: Number(file.created_at ?? 0),
4853
+ updatedAt: Number(file.updated_at ?? 0)
4854
+ }));
4855
+ }
4856
+ case "read_channel_file": {
4857
+ const content = p;
4858
+ return {
4859
+ path: String(content.path ?? ""),
4860
+ filename: String(content.filename ?? ""),
4861
+ contentType: String(content.content_type ?? ""),
4862
+ sizeBytes: Number(content.size_bytes ?? 0),
4863
+ ...content.base64 != null ? { base64: String(content.base64) } : {},
4864
+ ...content.text != null ? { text: String(content.text) } : {}
4865
+ };
4866
+ }
4867
+ case "publish_channel_file": {
4868
+ const file = p;
4869
+ return {
4870
+ id: String(file.id ?? ""),
4871
+ ownerUserId: String(file.owner_user_id ?? ""),
4872
+ channelId: String(file.channel_id ?? ""),
4873
+ path: String(file.path ?? ""),
4874
+ parentPath: String(file.parent_path ?? ""),
4875
+ name: String(file.name ?? ""),
4876
+ isDirectory: Boolean(file.is_directory),
4877
+ mimeType: String(file.mime_type ?? ""),
4878
+ sizeBytes: Number(file.size_bytes ?? 0),
4879
+ source: String(file.source ?? ""),
4880
+ sourceMessageId: String(file.source_message_id ?? ""),
4881
+ createdAt: Number(file.created_at ?? 0),
4882
+ updatedAt: Number(file.updated_at ?? 0)
4883
+ };
4884
+ }
4826
4885
  case "list_users": {
4827
4886
  const arr = Array.isArray(p) ? p : [];
4828
4887
  return arr.map((u) => ({
@@ -5167,6 +5226,15 @@ var Client = class {
5167
5226
  async listUsers() {
5168
5227
  return await this.t.perform({ op: "list_users" });
5169
5228
  }
5229
+ async listChannelFiles(input) {
5230
+ return await this.t.perform({ op: "list_channel_files", channelId: input.channelId });
5231
+ }
5232
+ async readChannelFile(input) {
5233
+ return await this.t.perform({ op: "read_channel_file", ...input });
5234
+ }
5235
+ async publishChannelFile(input) {
5236
+ return await this.t.perform({ op: "publish_channel_file", ...input });
5237
+ }
5170
5238
  async listChannels() {
5171
5239
  return await this.t.perform({ op: "list_channels" });
5172
5240
  }
@@ -5307,10 +5375,11 @@ var Client = class {
5307
5375
  async listTaskPropertyDefinitions() {
5308
5376
  return await this.t.perform({ op: "list_task_property_definitions" });
5309
5377
  }
5378
+ reportTyping(channelId) {
5379
+ void this.t.perform({ op: "report_typing", channelId }).catch((err) => this.logger.warn("reportTyping failed", err));
5380
+ }
5310
5381
  startTyping(channelId) {
5311
- const emit = () => {
5312
- void this.t.perform({ op: "report_typing", channelId }).catch((err) => this.logger.warn("startTyping failed", err));
5313
- };
5382
+ const emit = () => this.reportTyping(channelId);
5314
5383
  emit();
5315
5384
  const interval = setInterval(emit, 2e3);
5316
5385
  interval.unref?.();