@borgee/agents-host 0.2.35 → 0.2.44

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 (42) hide show
  1. package/dist/agents-host.d.ts +2 -0
  2. package/dist/agents-host.js +111 -44
  3. package/dist/chat/sdk-chat-control-plane.js +3 -0
  4. package/dist/context/injection.d.ts +5 -3
  5. package/dist/context/injection.js +45 -29
  6. package/dist/context/prompt.js +23 -35
  7. package/dist/context/skill-manual.d.ts +13 -0
  8. package/dist/context/skill-manual.js +18 -0
  9. package/dist/context/turn-preparation.js +13 -4
  10. package/dist/gateway/localhost-gateway.js +16 -8
  11. package/dist/hosted-turn-content.d.ts +15 -0
  12. package/dist/hosted-turn-content.js +50 -0
  13. package/dist/managed-daemon.d.ts +3 -2
  14. package/dist/managed-daemon.js +77 -29
  15. package/dist/providers/claude/adapter.d.ts +3 -1
  16. package/dist/providers/claude/adapter.js +10 -0
  17. package/dist/providers/claude/cli-client.d.ts +11 -2
  18. package/dist/providers/claude/cli-client.js +98 -27
  19. package/dist/providers/codex/adapter.d.ts +3 -1
  20. package/dist/providers/codex/adapter.js +10 -0
  21. package/dist/providers/codex/cli-client.d.ts +10 -2
  22. package/dist/providers/codex/cli-client.js +85 -21
  23. package/dist/providers/codex/project-doc.js +13 -29
  24. package/dist/providers/copilot/adapter.d.ts +3 -1
  25. package/dist/providers/copilot/adapter.js +10 -0
  26. package/dist/providers/copilot/cli-client.d.ts +9 -1
  27. package/dist/providers/copilot/cli-client.js +71 -8
  28. package/dist/providers/create-provider.d.ts +1 -1
  29. package/dist/providers/create-provider.js +16 -5
  30. package/dist/providers/provider-adapter.d.ts +35 -0
  31. package/dist/providers/provider-adapter.js +44 -1
  32. package/dist/state-paths.d.ts +9 -1
  33. package/dist/state-paths.js +22 -3
  34. package/dist/types.d.ts +33 -2
  35. package/package.json +1 -1
  36. package/skills/borgee-agent/SKILL.md +119 -38
  37. package/skills/borgee-agent/references/errors.md +38 -0
  38. package/skills/borgee-agent/references/task-properties.md +30 -0
  39. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  40. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  41. package/skills/borgee-agent/borgee-agent.mjs +0 -562
  42. package/skills/borgee-agent/borgee-agent.py +0 -469
@@ -93,9 +93,11 @@ export declare class AgentsHost {
93
93
  private armProtocolFallback;
94
94
  private runProtocolFallback;
95
95
  private handleProtocolFallbackFailure;
96
+ private ensureProtocolAnchorIncomingParts;
96
97
  private finishProtocolState;
97
98
  private cancelProtocolState;
98
99
  private confirmProtocolMessage;
100
+ private findChannelHistoryMessageById;
99
101
  private resolveAttentionStatePath;
100
102
  private buildDefaultAttentionSnapshot;
101
103
  /**
@@ -14,6 +14,7 @@ import { createProvider } from './providers/create-provider.js';
14
14
  import { resolveAttentionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
15
15
  import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
16
16
  import { waitForTaskForThread } from './task-thread-resolution.js';
17
+ import { buildHostedIncomingContentText, buildHostedTurnContentParts, extractHostedMessageAttachments, normalizeHostedMessageAttachment, } from './hosted-turn-content.js';
17
18
  const PRIVATE_STATE_ROOT_MODE = 0o700;
18
19
  const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
19
20
  const PROTOCOL_STATE_DIRNAME = 'protocol-state';
@@ -126,7 +127,9 @@ class DefaultHostRuntime {
126
127
  deps.provider ??
127
128
  createProvider({
128
129
  provider: config.agent.provider,
130
+ borgeeBaseUrl: config.borgeeBaseUrl,
129
131
  stateRootDir: config.stateRootDir,
132
+ agentApiKey: config.agent.agentApiKey,
130
133
  resolveStableAgentId: deps.resolveStableAgentId,
131
134
  claudeCommand: config.claudeCommand,
132
135
  claudeArgs: config.claudeArgs,
@@ -559,7 +562,7 @@ export class AgentsHost {
559
562
  }
560
563
  }
561
564
  }
562
- await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot);
565
+ await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot, normalized.incomingParts);
563
566
  }
564
567
  async handleCollaborationMessage(msg) {
565
568
  const normalized = await this.normalizeMessage(msg);
@@ -608,6 +611,7 @@ export class AgentsHost {
608
611
  authorId: normalized.authorId,
609
612
  authorKind: 'unknown',
610
613
  content: normalized.content,
614
+ incomingParts: normalized.incomingParts,
611
615
  attentionSnapshot,
612
616
  taskThreadContext,
613
617
  });
@@ -626,6 +630,7 @@ export class AgentsHost {
626
630
  authorId: normalized.authorId,
627
631
  authorKind: 'human',
628
632
  content: normalized.content,
633
+ incomingParts: normalized.incomingParts,
629
634
  attentionSnapshot,
630
635
  taskThreadContext,
631
636
  wakeBlockedState,
@@ -658,6 +663,7 @@ export class AgentsHost {
658
663
  authorId: normalized.authorId,
659
664
  authorKind,
660
665
  content: normalized.content,
666
+ incomingParts: normalized.incomingParts,
661
667
  attentionSnapshot,
662
668
  taskThreadContext,
663
669
  });
@@ -752,6 +758,7 @@ export class AgentsHost {
752
758
  const executed = await this.executeTurn(msg, normalized.authorId, normalized.content, {
753
759
  kickoff: protocolCandidate,
754
760
  silent: true,
761
+ incomingParts: normalized.incomingParts,
755
762
  });
756
763
  if (executed.stale) {
757
764
  return null;
@@ -912,6 +919,7 @@ export class AgentsHost {
912
919
  authorId: normalized.authorId,
913
920
  authorKind: 'human',
914
921
  content: normalized.content,
922
+ incomingParts: normalized.incomingParts,
915
923
  });
916
924
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
917
925
  return;
@@ -924,6 +932,7 @@ export class AgentsHost {
924
932
  ownerAgentId: selfAgentId,
925
933
  anchorAuthorId: normalized.authorId,
926
934
  anchorContent: normalized.content,
935
+ anchorIncomingParts: normalized.incomingParts,
927
936
  anchorMessageId: msg.message_id ?? randomUUID(),
928
937
  participantIds: protocolRequest.participantIds,
929
938
  issuerId: protocolRequest.issuerId,
@@ -946,6 +955,7 @@ export class AgentsHost {
946
955
  authorId: normalized.authorId,
947
956
  authorKind: 'human',
948
957
  content: normalized.content,
958
+ incomingParts: normalized.incomingParts,
949
959
  });
950
960
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
951
961
  return;
@@ -1014,6 +1024,7 @@ export class AgentsHost {
1014
1024
  authorId: normalized.authorId,
1015
1025
  authorKind: 'agent',
1016
1026
  content: normalized.content,
1027
+ incomingParts: normalized.incomingParts,
1017
1028
  });
1018
1029
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
1019
1030
  }
@@ -1131,6 +1142,7 @@ export class AgentsHost {
1131
1142
  channelState.activeTurn = activeTurn;
1132
1143
  const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
1133
1144
  activeTurn,
1145
+ incomingParts: message.incomingParts,
1134
1146
  incomingAuthorKind: message.authorKind,
1135
1147
  attentionSnapshot,
1136
1148
  taskThreadContext,
@@ -1234,6 +1246,7 @@ export class AgentsHost {
1234
1246
  channelState.activeTurn = activeTurn;
1235
1247
  const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
1236
1248
  activeTurn,
1249
+ incomingParts: message.incomingParts,
1237
1250
  incomingAuthorKind: message.authorKind,
1238
1251
  attentionSnapshot,
1239
1252
  taskThreadContext,
@@ -1541,59 +1554,52 @@ export class AgentsHost {
1541
1554
  }
1542
1555
  async buildResumedProtocolMessage(channelId, protocolState) {
1543
1556
  if (protocolState.currentTurnIndex === 0) {
1557
+ const anchorIncomingParts = await this.ensureProtocolAnchorIncomingParts(channelId, protocolState);
1558
+ const anchorIncomingContent = buildHostedIncomingContentText(anchorIncomingParts);
1544
1559
  return {
1545
1560
  kind: 'protocol-turn',
1546
1561
  raw: {
1547
1562
  channel_id: channelId,
1548
1563
  message_id: protocolState.anchorMessageId,
1549
1564
  user_id: protocolState.anchorAuthorId,
1550
- content: protocolState.anchorContent,
1551
- body: protocolState.anchorContent,
1565
+ content: anchorIncomingContent,
1566
+ body: anchorIncomingContent,
1567
+ attachments: extractHostedMessageAttachments(anchorIncomingParts),
1552
1568
  type: 'message',
1553
1569
  },
1554
1570
  authorId: protocolState.anchorAuthorId,
1555
1571
  authorKind: 'human',
1556
- content: protocolState.anchorContent,
1572
+ content: anchorIncomingContent,
1573
+ incomingParts: anchorIncomingParts,
1557
1574
  };
1558
1575
  }
1559
1576
  if (!protocolState.lastProtocolMessageId) {
1560
1577
  return null;
1561
1578
  }
1562
- let before;
1563
- for (;;) {
1564
- const history = await this.borgee.readChannelHistory({
1565
- channelId,
1566
- before,
1567
- limit: 200,
1579
+ const priorMessage = await this.findChannelHistoryMessageById(channelId, protocolState.lastProtocolMessageId);
1580
+ if (priorMessage) {
1581
+ const incomingParts = buildHostedTurnContentParts({
1582
+ text: priorMessage.body,
1583
+ attachments: priorMessage.attachments,
1568
1584
  });
1569
- const priorMessage = history.find((entry) => entry.id === protocolState.lastProtocolMessageId);
1570
- if (priorMessage) {
1571
- return {
1572
- kind: 'protocol-turn',
1573
- raw: {
1574
- channel_id: channelId,
1575
- message_id: priorMessage.id,
1576
- user_id: priorMessage.authorId,
1577
- content: priorMessage.body,
1578
- body: priorMessage.body,
1579
- type: 'message',
1580
- created_at: priorMessage.createdAt,
1581
- },
1582
- authorId: priorMessage.authorId,
1583
- authorKind: 'agent',
1584
- content: priorMessage.body,
1585
- };
1586
- }
1587
- if (history.length < 200) {
1588
- break;
1589
- }
1590
- const oldestCreatedAt = history.reduce((oldest, entry) => {
1591
- return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
1592
- }, null);
1593
- if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
1594
- break;
1595
- }
1596
- before = oldestCreatedAt;
1585
+ const incomingContent = buildHostedIncomingContentText(incomingParts);
1586
+ return {
1587
+ kind: 'protocol-turn',
1588
+ raw: {
1589
+ channel_id: channelId,
1590
+ message_id: priorMessage.id,
1591
+ user_id: priorMessage.authorId,
1592
+ content: incomingContent,
1593
+ body: incomingContent,
1594
+ attachments: priorMessage.attachments,
1595
+ type: 'message',
1596
+ created_at: priorMessage.createdAt,
1597
+ },
1598
+ authorId: priorMessage.authorId,
1599
+ authorKind: 'agent',
1600
+ content: incomingContent,
1601
+ incomingParts,
1602
+ };
1597
1603
  }
1598
1604
  return null;
1599
1605
  }
@@ -1604,6 +1610,7 @@ export class AgentsHost {
1604
1610
  ownerAgentId,
1605
1611
  anchorAuthorId: protocolState.anchorAuthorId,
1606
1612
  anchorContent: protocolState.anchorContent,
1613
+ anchorIncomingParts: protocolState.anchorIncomingParts,
1607
1614
  anchorMessageId: protocolState.anchorMessageId,
1608
1615
  participantIds: protocolState.participantIds,
1609
1616
  issuerId: protocolState.issuerId,
@@ -1682,18 +1689,36 @@ export class AgentsHost {
1682
1689
  await this.persistProtocolState(channelId, protocolState);
1683
1690
  return;
1684
1691
  }
1692
+ const anchorIncomingParts = await this.ensureProtocolAnchorIncomingParts(channelId, protocolState);
1693
+ const anchorIncomingContent = buildHostedIncomingContentText(anchorIncomingParts);
1685
1694
  const fallbackMessage = {
1686
1695
  channel_id: channelId,
1687
1696
  message_id: protocolState.anchorMessageId,
1688
1697
  user_id: protocolState.anchorAuthorId,
1689
- content: protocolState.anchorContent,
1690
- body: protocolState.anchorContent,
1698
+ content: anchorIncomingContent,
1699
+ body: anchorIncomingContent,
1700
+ attachments: extractHostedMessageAttachments(anchorIncomingParts),
1691
1701
  type: 'message',
1692
1702
  };
1693
1703
  this.cancelProtocolState(channelId, channelState);
1694
1704
  const taskThreadContext = await this.buildTurnTaskThreadContext(fallbackMessage, channelId);
1695
1705
  const attentionSnapshot = this.getAttentionSnapshotForTurn(channelId, taskThreadContext);
1696
- await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, protocolState.anchorContent, taskThreadContext, attentionSnapshot);
1706
+ await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, anchorIncomingContent, taskThreadContext, attentionSnapshot, anchorIncomingParts);
1707
+ }
1708
+ async ensureProtocolAnchorIncomingParts(channelId, protocolState) {
1709
+ if (protocolState.anchorIncomingParts) {
1710
+ return protocolState.anchorIncomingParts;
1711
+ }
1712
+ const anchorMessage = await this.findChannelHistoryMessageById(channelId, protocolState.anchorMessageId);
1713
+ if (!anchorMessage) {
1714
+ return buildHostedTurnContentParts({ text: protocolState.anchorContent });
1715
+ }
1716
+ const anchorIncomingParts = buildHostedTurnContentParts({
1717
+ text: protocolState.anchorContent,
1718
+ attachments: anchorMessage.attachments,
1719
+ });
1720
+ protocolState.anchorIncomingParts = anchorIncomingParts;
1721
+ return anchorIncomingParts;
1697
1722
  }
1698
1723
  finishProtocolState(channelId, channelState, protocolState, lastMessageId) {
1699
1724
  protocolState.active = false;
@@ -1733,6 +1758,36 @@ export class AgentsHost {
1733
1758
  }
1734
1759
  return matched;
1735
1760
  }
1761
+ async findChannelHistoryMessageById(channelId, messageId) {
1762
+ let before;
1763
+ for (;;) {
1764
+ const history = await this.borgee.readChannelHistory({
1765
+ channelId,
1766
+ before,
1767
+ limit: 200,
1768
+ });
1769
+ const matched = history.find((entry) => entry.id === messageId);
1770
+ if (matched) {
1771
+ return {
1772
+ ...matched,
1773
+ ...(matched.attachments
1774
+ ? { attachments: matched.attachments.map(normalizeHostedMessageAttachment) }
1775
+ : {}),
1776
+ };
1777
+ }
1778
+ if (history.length < 200) {
1779
+ break;
1780
+ }
1781
+ const oldestCreatedAt = history.reduce((oldest, entry) => {
1782
+ return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
1783
+ }, null);
1784
+ if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
1785
+ break;
1786
+ }
1787
+ before = oldestCreatedAt;
1788
+ }
1789
+ return null;
1790
+ }
1736
1791
  resolveAttentionStatePath(channelId) {
1737
1792
  return resolveAttentionStatePath(this.config.stateRootDir, channelId);
1738
1793
  }
@@ -2037,10 +2092,11 @@ export class AgentsHost {
2037
2092
  reason: 'wake-suppressed-attention-policy',
2038
2093
  });
2039
2094
  }
2040
- async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot) {
2095
+ async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot, incomingParts) {
2041
2096
  const executed = await this.executeTurn(msg, authorId, content, {
2042
2097
  attentionSnapshot,
2043
2098
  taskThreadContext,
2099
+ incomingParts,
2044
2100
  });
2045
2101
  if (executed.completed && executed.reply) {
2046
2102
  this.updateAwaitingUserState(msg.channel_id, executed.reply.awaitingUser);
@@ -2133,6 +2189,11 @@ export class AgentsHost {
2133
2189
  : undefined;
2134
2190
  const missedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(channelId);
2135
2191
  const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn();
2192
+ const incomingParts = options?.incomingParts
2193
+ ?? buildHostedTurnContentParts({
2194
+ text: String(msg.content ?? msg.body ?? content),
2195
+ attachments: msg.attachments,
2196
+ });
2136
2197
  try {
2137
2198
  reply = await this.provider.generateReply({
2138
2199
  agentName: this.config.agent.agentName,
@@ -2140,6 +2201,7 @@ export class AgentsHost {
2140
2201
  channelId,
2141
2202
  incomingAuthorId: authorId,
2142
2203
  incomingContent: content,
2204
+ incomingParts,
2143
2205
  incomingEventKind: msg.type,
2144
2206
  incomingMessageType: msg.message_type,
2145
2207
  collaboration: turnExecutionId || options?.kickoff
@@ -2683,17 +2745,22 @@ export class AgentsHost {
2683
2745
  });
2684
2746
  return null;
2685
2747
  }
2686
- const content = String(msg.content ?? msg.body ?? '').trim();
2687
- if (!content) {
2748
+ const incomingParts = buildHostedTurnContentParts({
2749
+ text: String(msg.content ?? msg.body ?? ''),
2750
+ attachments: msg.attachments,
2751
+ });
2752
+ if (incomingParts.length === 0) {
2688
2753
  this.logger.debug('ignored empty message', {
2689
2754
  channelId: msg.channel_id,
2690
2755
  messageId: msg.message_id,
2691
2756
  });
2692
2757
  return null;
2693
2758
  }
2759
+ const content = buildHostedIncomingContentText(incomingParts);
2694
2760
  return {
2695
2761
  authorId,
2696
2762
  content,
2763
+ incomingParts,
2697
2764
  };
2698
2765
  }
2699
2766
  // Only a task assigned to this agent drives a task-assignment turn: it keeps the message a
@@ -1,4 +1,5 @@
1
1
  import { createBorgeePlugin, } from '../plugin-sdk.js';
2
+ import { normalizeHostedMessageAttachment } from '../hosted-turn-content.js';
2
3
  /**
3
4
  * Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
4
5
  * the OpenClaw plugin) that implements the minimal `ChatControlPlane` surface
@@ -97,6 +98,7 @@ export class SdkChatControlPlane {
97
98
  authorId: message.authorId,
98
99
  body: message.body,
99
100
  contentType: message.contentType,
101
+ attachments: message.attachments?.map(normalizeHostedMessageAttachment),
100
102
  type: message.type,
101
103
  replyToId: message.replyToId,
102
104
  createdAt: message.createdAt,
@@ -142,6 +144,7 @@ export function mapInboundToChannelMessage(event) {
142
144
  content: event.message?.body,
143
145
  body: event.message?.body,
144
146
  content_type: event.message?.contentType,
147
+ attachments: event.message?.attachments?.map(normalizeHostedMessageAttachment),
145
148
  created_at: event.createdAt,
146
149
  };
147
150
  }
@@ -21,7 +21,7 @@ export interface PreparedChannelContext {
21
21
  directoryPath: string;
22
22
  payload: ChannelContextPayload;
23
23
  payloadPath?: string;
24
- gatewayAuthPath?: string;
24
+ gatewayCredentialPath?: string;
25
25
  skillRuntime?: SkillRuntimeBootstrapMetadata;
26
26
  localhostGateway?: LocalhostGatewayBootstrapMetadata;
27
27
  taskWorkspace?: TaskWorkspaceContext;
@@ -83,6 +83,7 @@ interface ContextFileSystem {
83
83
  mode?: number;
84
84
  }): Promise<void>;
85
85
  unlink(path: string): Promise<void>;
86
+ rename(oldPath: string, newPath: string): Promise<void>;
86
87
  access(path: string): Promise<void>;
87
88
  }
88
89
  interface FileChannelContextStoreOptions {
@@ -98,8 +99,9 @@ export declare function resolveChannelContextDirectory(stateRootDir: string, cha
98
99
  export declare function resolveChannelContextPayloadPath(stateRootDir: string, channelId: string): string;
99
100
  export declare function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir: string): string;
100
101
  export declare function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir: string, channelId: string, taskId: string): string;
101
- export declare function resolveChannelContextGatewayAuthPath(stateRootDir: string, channelId: string, turnExecutionId?: string): string;
102
- export declare function resolveGatewayAuthPathFromPayloadPath(payloadPath: string): string;
102
+ export declare function resolveChannelGatewayCredentialPath(stateRootDir: string, channelId: string): string;
103
+ export declare function resolveGatewayCredentialPathFromPayloadPath(payloadPath: string): string;
104
+ export declare function isGatewayCredentialSidecarBasename(basename: string): boolean;
103
105
  export declare function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl: string, fileSystem?: Pick<ContextFileSystem, 'access'>): Promise<SkillRuntimeBootstrapMetadata>;
104
106
  export declare class FileChannelContextStore implements ChannelContextStore {
105
107
  private readonly stateRootDir;
@@ -1,9 +1,10 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  const CHANNEL_CONTEXT_ROOT_DIRNAME = 'channel-context';
5
6
  const CHANNEL_CONTEXT_PAYLOAD_FILENAME = 'context.json';
6
- const CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME = '.localhost-gateway-auth.json';
7
+ const CHANNEL_GATEWAY_CREDENTIAL_FILENAME = '.borgee-agent-gateway.json';
7
8
  const TASK_WORKSPACE_ROOT_DIRNAME = '.borgee-task-workspaces';
8
9
  export class ChannelContextPreparationError extends Error {
9
10
  partialContext;
@@ -29,6 +30,9 @@ const DEFAULT_FILE_SYSTEM = {
29
30
  async unlink(path) {
30
31
  await fs.unlink(path);
31
32
  },
33
+ async rename(oldPath, newPath) {
34
+ await fs.rename(oldPath, newPath);
35
+ },
32
36
  async access(path) {
33
37
  await fs.access(path);
34
38
  },
@@ -45,7 +49,7 @@ function toSkillRuntimePayload(skillRuntime) {
45
49
  pythonCliPath: skillRuntime.pythonCliPath,
46
50
  };
47
51
  }
48
- function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
52
+ function buildBorgeeAgentGatewayCredential(channelId, localhostGateway) {
49
53
  if (!localhostGateway) {
50
54
  return undefined;
51
55
  }
@@ -53,6 +57,7 @@ function buildLocalhostGatewayAuthPayload(channelId, localhostGateway) {
53
57
  schemaVersion: 1,
54
58
  channelId,
55
59
  localhostGateway: {
60
+ baseUrl: localhostGateway.baseUrl,
56
61
  token: localhostGateway.token,
57
62
  },
58
63
  };
@@ -144,17 +149,28 @@ export function resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir) {
144
149
  export function resolveTaskWorkspaceDirectory(startupWorkspaceRootDir, channelId, taskId) {
145
150
  return join(resolveTaskWorkspaceRootDirectory(startupWorkspaceRootDir), encodeChannelPathSegment(channelId), encodeChannelPathSegment(taskId));
146
151
  }
147
- export function resolveChannelContextGatewayAuthPath(stateRootDir, channelId, turnExecutionId) {
148
- const filename = turnExecutionId
149
- ? `.localhost-gateway-auth.${Buffer.from(turnExecutionId, 'utf8').toString('hex')}.json`
150
- : CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME;
151
- return join(resolveChannelContextDirectory(stateRootDir, channelId), filename);
152
+ export function resolveChannelGatewayCredentialPath(stateRootDir, channelId) {
153
+ return join(resolveChannelContextDirectory(stateRootDir, channelId), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
154
+ }
155
+ export function resolveGatewayCredentialPathFromPayloadPath(payloadPath) {
156
+ return join(dirname(payloadPath), CHANNEL_GATEWAY_CREDENTIAL_FILENAME);
152
157
  }
153
- export function resolveGatewayAuthPathFromPayloadPath(payloadPath) {
154
- return join(dirname(payloadPath), CHANNEL_CONTEXT_GATEWAY_AUTH_FILENAME);
158
+ /**
159
+ * Every basename a gateway credential can carry in a channel context directory, including the ones
160
+ * this host no longer writes. The directory holds at most one live credential — the token is
161
+ * reissued every turn — and this sweep is the only thing that ever unlinks the others, so a name
162
+ * dropped from this list leaves a 0600 file behind that still looks live.
163
+ */
164
+ const GATEWAY_CREDENTIAL_SIDECAR_BASENAME_PREFIXES = [
165
+ '.borgee-agent-gateway.',
166
+ '.borgee-agent-bootstrap.',
167
+ '.localhost-gateway-auth.',
168
+ ];
169
+ export function isGatewayCredentialSidecarBasename(basename) {
170
+ return basename.endsWith('.json')
171
+ && GATEWAY_CREDENTIAL_SIDECAR_BASENAME_PREFIXES.some((prefix) => basename.startsWith(prefix));
155
172
  }
156
- const LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX = '.localhost-gateway-auth.';
157
- async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
173
+ async function pruneGatewayCredentialSidecars(fileSystem, directoryPath, keepBasename) {
158
174
  let entries;
159
175
  try {
160
176
  entries = await fileSystem.readdir(directoryPath);
@@ -167,8 +183,7 @@ async function pruneLegacyGatewayAuthSidecars(fileSystem, directoryPath) {
167
183
  throw error;
168
184
  }
169
185
  await Promise.all(entries
170
- .filter((entry) => entry.startsWith(LEGACY_TURN_SCOPED_GATEWAY_AUTH_BASENAME_PREFIX)
171
- && entry.endsWith('.json'))
186
+ .filter((entry) => isGatewayCredentialSidecarBasename(entry) && entry !== keepBasename)
172
187
  .map(async (entry) => {
173
188
  await fileSystem.unlink(join(directoryPath, entry)).catch((error) => {
174
189
  if (error.code !== 'ENOENT') {
@@ -196,8 +211,8 @@ export async function resolveBorgeeAgentSkillRuntimeAssets(moduleUrl, fileSystem
196
211
  const packageRootPath = await resolvePackageRootFromModuleUrl(moduleUrl, fileSystem);
197
212
  const skillDirectoryPath = join(packageRootPath, 'skills', BORGEE_AGENT_SKILL_DIRNAME);
198
213
  const skillMarkdownPath = join(skillDirectoryPath, 'SKILL.md');
199
- const nodeCliPath = join(skillDirectoryPath, 'borgee-agent.mjs');
200
- const pythonCliPath = join(skillDirectoryPath, 'borgee-agent.py');
214
+ const nodeCliPath = join(skillDirectoryPath, 'scripts', 'borgee-agent.mjs');
215
+ const pythonCliPath = join(skillDirectoryPath, 'scripts', 'borgee-agent.py');
201
216
  await Promise.all([
202
217
  fileSystem.access(skillDirectoryPath),
203
218
  fileSystem.access(skillMarkdownPath),
@@ -249,7 +264,7 @@ export class FileChannelContextStore {
249
264
  const auxiliaryCollaborationEnabled = collaborationCommandsEnabled && collaborationRoutesAllowedForTurnMode;
250
265
  const directoryPath = resolveChannelContextDirectory(this.stateRootDir, input.channelId);
251
266
  const payloadPath = resolveChannelContextPayloadPath(this.stateRootDir, input.channelId);
252
- const gatewayAuthPath = resolveGatewayAuthPathFromPayloadPath(payloadPath);
267
+ const gatewayCredentialPath = resolveGatewayCredentialPathFromPayloadPath(payloadPath);
253
268
  const existingTaskAssignmentContext = await readExistingTaskAssignmentContext(payloadPath, this.fileSystem);
254
269
  const skillRuntime = this.skillRuntimeEnabled
255
270
  ? await this.resolveSkillRuntimeBestEffort()
@@ -292,15 +307,15 @@ export class FileChannelContextStore {
292
307
  ...(taskWorkspace ? { taskWorkspace } : {}),
293
308
  }
294
309
  : undefined);
295
- const gatewayAuthPayload = buildLocalhostGatewayAuthPayload(input.channelId, issuedLocalhostGateway);
310
+ const gatewayCredential = buildBorgeeAgentGatewayCredential(input.channelId, issuedLocalhostGateway);
296
311
  let taskWorkspaceMaterialized = false;
297
312
  let payloadWritten = false;
298
- let gatewayAuthWritten = false;
313
+ let gatewayCredentialWritten = false;
299
314
  const preparedContext = {
300
315
  directoryPath,
301
316
  payload,
302
317
  payloadPath,
303
- gatewayAuthPath: gatewayAuthPayload ? gatewayAuthPath : undefined,
318
+ gatewayCredentialPath: gatewayCredential ? gatewayCredentialPath : undefined,
304
319
  skillRuntime,
305
320
  localhostGateway,
306
321
  taskWorkspace,
@@ -311,19 +326,20 @@ export class FileChannelContextStore {
311
326
  await this.fileSystem.mkdir(taskWorkspace.rootPath, { recursive: true, mode: 0o700 });
312
327
  taskWorkspaceMaterialized = true;
313
328
  }
314
- await pruneLegacyGatewayAuthSidecars(this.fileSystem, directoryPath);
329
+ await pruneGatewayCredentialSidecars(this.fileSystem, directoryPath, gatewayCredential ? CHANNEL_GATEWAY_CREDENTIAL_FILENAME : undefined);
315
330
  await this.fileSystem.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
316
331
  payloadWritten = true;
317
- if (gatewayAuthPayload) {
318
- await this.fileSystem.writeFile(gatewayAuthPath, `${JSON.stringify(gatewayAuthPayload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
319
- gatewayAuthWritten = true;
332
+ if (gatewayCredential) {
333
+ // Replaced atomically: a CLI invocation left over from an earlier turn must read either
334
+ // the old credential or the new one, never a missing or half-written file, because the
335
+ // documented correction for a rotated token is a 401 and not a usage error. The staging
336
+ // name is itself a credential sidecar, so a crashed run's leftover is swept above.
337
+ const stagingCredentialPath = join(directoryPath, `.borgee-agent-gateway.${randomUUID()}.json`);
338
+ await this.fileSystem.writeFile(stagingCredentialPath, `${JSON.stringify(gatewayCredential, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
339
+ await this.fileSystem.rename(stagingCredentialPath, gatewayCredentialPath);
340
+ gatewayCredentialWritten = true;
320
341
  }
321
342
  else {
322
- await this.fileSystem.unlink(gatewayAuthPath).catch((error) => {
323
- if (error.code !== 'ENOENT') {
324
- throw error;
325
- }
326
- });
327
343
  this.localhostGateway?.clearChannel(input.channelId);
328
344
  }
329
345
  if (issuedLocalhostGateway) {
@@ -336,7 +352,7 @@ export class FileChannelContextStore {
336
352
  directoryPath,
337
353
  payload,
338
354
  payloadPath: payloadWritten ? payloadPath : undefined,
339
- gatewayAuthPath: gatewayAuthWritten ? gatewayAuthPath : undefined,
355
+ gatewayCredentialPath: gatewayCredentialWritten ? gatewayCredentialPath : undefined,
340
356
  skillRuntime: payloadWritten ? skillRuntime : undefined,
341
357
  localhostGateway: payloadWritten ? localhostGateway : undefined,
342
358
  taskWorkspace: taskWorkspaceMaterialized ? taskWorkspace : undefined,