agent-relay 1.2.3 → 1.3.1

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 (189) hide show
  1. package/.trajectories/agent-relay-322-324.md +17 -0
  2. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
  5. package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
  6. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
  7. package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
  8. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
  9. package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
  10. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
  11. package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
  12. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
  13. package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
  14. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
  15. package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
  16. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
  17. package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
  18. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
  19. package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
  20. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
  21. package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
  22. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
  23. package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
  24. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
  25. package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
  26. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
  27. package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
  28. package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
  29. package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
  30. package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
  31. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
  32. package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
  33. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
  34. package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
  35. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
  36. package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
  37. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
  38. package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
  39. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
  40. package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
  41. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
  42. package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
  43. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
  44. package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
  45. package/.trajectories/consolidate-settings-panel.md +24 -0
  46. package/.trajectories/gh-cli-user-token.md +26 -0
  47. package/.trajectories/index.json +155 -1
  48. package/deploy/workspace/codex.config.toml +15 -0
  49. package/deploy/workspace/entrypoint.sh +167 -7
  50. package/deploy/workspace/git-credential-relay +17 -2
  51. package/dist/bridge/spawner.d.ts +7 -0
  52. package/dist/bridge/spawner.js +40 -9
  53. package/dist/bridge/types.d.ts +2 -0
  54. package/dist/cli/index.js +210 -168
  55. package/dist/cloud/api/admin.d.ts +8 -0
  56. package/dist/cloud/api/admin.js +212 -0
  57. package/dist/cloud/api/auth.js +8 -0
  58. package/dist/cloud/api/billing.d.ts +0 -10
  59. package/dist/cloud/api/billing.js +248 -58
  60. package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
  61. package/dist/cloud/api/codex-auth-helper.js +215 -8
  62. package/dist/cloud/api/coordinators.js +402 -0
  63. package/dist/cloud/api/daemons.js +15 -11
  64. package/dist/cloud/api/git.js +104 -17
  65. package/dist/cloud/api/github-app.js +42 -8
  66. package/dist/cloud/api/nango-auth.js +297 -16
  67. package/dist/cloud/api/onboarding.js +97 -33
  68. package/dist/cloud/api/providers.js +12 -16
  69. package/dist/cloud/api/repos.js +200 -124
  70. package/dist/cloud/api/test-helpers.js +40 -0
  71. package/dist/cloud/api/usage.js +13 -0
  72. package/dist/cloud/api/webhooks.js +1 -1
  73. package/dist/cloud/api/workspaces.d.ts +18 -0
  74. package/dist/cloud/api/workspaces.js +945 -15
  75. package/dist/cloud/config.d.ts +8 -0
  76. package/dist/cloud/config.js +15 -0
  77. package/dist/cloud/db/drizzle.d.ts +5 -2
  78. package/dist/cloud/db/drizzle.js +27 -20
  79. package/dist/cloud/db/schema.d.ts +19 -51
  80. package/dist/cloud/db/schema.js +5 -4
  81. package/dist/cloud/index.d.ts +0 -1
  82. package/dist/cloud/index.js +0 -1
  83. package/dist/cloud/provisioner/index.d.ts +93 -1
  84. package/dist/cloud/provisioner/index.js +608 -63
  85. package/dist/cloud/server.js +156 -16
  86. package/dist/cloud/services/compute-enforcement.d.ts +57 -0
  87. package/dist/cloud/services/compute-enforcement.js +175 -0
  88. package/dist/cloud/services/index.d.ts +2 -0
  89. package/dist/cloud/services/index.js +4 -0
  90. package/dist/cloud/services/intro-expiration.d.ts +55 -0
  91. package/dist/cloud/services/intro-expiration.js +211 -0
  92. package/dist/cloud/services/nango.d.ts +14 -0
  93. package/dist/cloud/services/nango.js +74 -14
  94. package/dist/cloud/services/ssh-security.d.ts +31 -0
  95. package/dist/cloud/services/ssh-security.js +63 -0
  96. package/dist/continuity/manager.d.ts +5 -0
  97. package/dist/continuity/manager.js +56 -2
  98. package/dist/daemon/api.d.ts +2 -0
  99. package/dist/daemon/api.js +214 -5
  100. package/dist/daemon/cli-auth.d.ts +13 -1
  101. package/dist/daemon/cli-auth.js +166 -47
  102. package/dist/daemon/connection.d.ts +7 -1
  103. package/dist/daemon/connection.js +15 -0
  104. package/dist/daemon/orchestrator.d.ts +2 -0
  105. package/dist/daemon/orchestrator.js +26 -0
  106. package/dist/daemon/repo-manager.d.ts +116 -0
  107. package/dist/daemon/repo-manager.js +384 -0
  108. package/dist/daemon/router.d.ts +60 -1
  109. package/dist/daemon/router.js +281 -20
  110. package/dist/daemon/user-directory.d.ts +111 -0
  111. package/dist/daemon/user-directory.js +233 -0
  112. package/dist/dashboard/out/404.html +1 -1
  113. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  114. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  115. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  117. package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +1 -0
  118. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/{page-3fdfa60e53f2810d.js → page-8553743baca53a00.js} +1 -1
  119. package/dist/dashboard/out/_next/static/chunks/app/app/page-c617745b81344f4f.js +1 -0
  120. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
  121. package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-dc786c183425c2ac.js} +1 -1
  122. package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
  123. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
  124. package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
  125. package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +1 -0
  126. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  127. package/dist/dashboard/out/_next/static/sDcbGRTYLcpPvyTs_rsNb/_ssgManifest.js +1 -0
  128. package/dist/dashboard/out/app/onboarding.html +1 -1
  129. package/dist/dashboard/out/app/onboarding.txt +3 -3
  130. package/dist/dashboard/out/app.html +1 -1
  131. package/dist/dashboard/out/app.txt +3 -3
  132. package/dist/dashboard/out/apple-icon.png +0 -0
  133. package/dist/dashboard/out/connect-repos.html +1 -1
  134. package/dist/dashboard/out/connect-repos.txt +2 -2
  135. package/dist/dashboard/out/history.html +1 -1
  136. package/dist/dashboard/out/history.txt +2 -2
  137. package/dist/dashboard/out/index.html +1 -1
  138. package/dist/dashboard/out/index.txt +3 -3
  139. package/dist/dashboard/out/login.html +2 -2
  140. package/dist/dashboard/out/login.txt +2 -2
  141. package/dist/dashboard/out/metrics.html +1 -1
  142. package/dist/dashboard/out/metrics.txt +3 -3
  143. package/dist/dashboard/out/pricing.html +2 -2
  144. package/dist/dashboard/out/pricing.txt +3 -3
  145. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  146. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  147. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  148. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  149. package/dist/dashboard/out/providers.html +1 -1
  150. package/dist/dashboard/out/providers.txt +3 -3
  151. package/dist/dashboard/out/signup.html +2 -2
  152. package/dist/dashboard/out/signup.txt +2 -2
  153. package/dist/dashboard-server/server.js +316 -12
  154. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  155. package/dist/dashboard-server/user-bridge.js +189 -0
  156. package/dist/protocol/channels.d.ts +205 -0
  157. package/dist/protocol/channels.js +154 -0
  158. package/dist/protocol/types.d.ts +13 -1
  159. package/dist/resiliency/provider-context.js +2 -0
  160. package/dist/shared/cli-auth-config.d.ts +19 -0
  161. package/dist/shared/cli-auth-config.js +58 -2
  162. package/dist/utils/agent-config.js +1 -1
  163. package/dist/wrapper/auth-detection.d.ts +49 -0
  164. package/dist/wrapper/auth-detection.js +192 -0
  165. package/dist/wrapper/base-wrapper.d.ts +153 -0
  166. package/dist/wrapper/base-wrapper.js +393 -0
  167. package/dist/wrapper/client.d.ts +7 -1
  168. package/dist/wrapper/client.js +3 -0
  169. package/dist/wrapper/index.d.ts +1 -0
  170. package/dist/wrapper/index.js +4 -3
  171. package/dist/wrapper/pty-wrapper.d.ts +62 -84
  172. package/dist/wrapper/pty-wrapper.js +154 -180
  173. package/dist/wrapper/tmux-wrapper.d.ts +41 -66
  174. package/dist/wrapper/tmux-wrapper.js +90 -134
  175. package/package.json +4 -2
  176. package/scripts/postinstall.js +11 -155
  177. package/scripts/test-interactive-terminal.sh +248 -0
  178. package/dist/cloud/vault/index.d.ts +0 -76
  179. package/dist/cloud/vault/index.js +0 -219
  180. package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
  182. package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
  183. package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
  186. package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
  187. package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
  188. package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
  189. /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → sDcbGRTYLcpPvyTs_rsNb}/_buildManifest.js +0 -0
@@ -2,12 +2,15 @@
2
2
  * Message router for the agent relay daemon.
3
3
  * Handles routing messages between agents, topic subscriptions, and broadcast.
4
4
  */
5
- import { type Envelope, type SendEnvelope, type AckPayload, type ShadowConfig, type SpeakOnTrigger } from '../protocol/types.js';
5
+ import { type Envelope, type SendEnvelope, type AckPayload, type ShadowConfig, type SpeakOnTrigger, type EntityType } from '../protocol/types.js';
6
+ import type { ChannelJoinPayload, ChannelLeavePayload, ChannelMessagePayload } from '../protocol/channels.js';
6
7
  import type { StorageAdapter } from '../storage/adapter.js';
7
8
  import type { AgentRegistry } from './agent-registry.js';
8
9
  export interface RoutableConnection {
9
10
  id: string;
10
11
  agentName?: string;
12
+ /** Entity type: 'agent' (default) or 'user' for human users */
13
+ entityType?: EntityType;
11
14
  cli?: string;
12
15
  program?: string;
13
16
  model?: string;
@@ -55,6 +58,12 @@ export declare class Router {
55
58
  private shadowsByPrimary;
56
59
  /** Reverse lookup: shadowAgent -> primaryAgent (for cleanup) */
57
60
  private primaryByShadow;
61
+ /** Channel membership: channel -> Set of member names */
62
+ private channels;
63
+ /** User entities (human users, not agents) */
64
+ private users;
65
+ /** Reverse lookup: member name -> Set of channels they're in */
66
+ private memberChannels;
58
67
  /** Default timeout for processing indicator (30 seconds) */
59
68
  private static readonly PROCESSING_TIMEOUT_MS;
60
69
  /** Callback when processing state changes (for real-time dashboard updates) */
@@ -78,6 +87,10 @@ export declare class Router {
78
87
  * Unregister a connection.
79
88
  */
80
89
  unregister(connection: RoutableConnection): void;
90
+ /**
91
+ * Remove a member from all channels they're in.
92
+ */
93
+ private removeFromAllChannels;
81
94
  /**
82
95
  * Subscribe an agent to a topic.
83
96
  */
@@ -206,6 +219,52 @@ export declare class Router {
206
219
  * Replay any pending (unacked) messages for a resumed session.
207
220
  */
208
221
  replayPending(connection: RoutableConnection): Promise<void>;
222
+ /**
223
+ * Handle a CHANNEL_JOIN message.
224
+ * Adds the member to the channel and notifies existing members.
225
+ */
226
+ handleChannelJoin(connection: RoutableConnection, envelope: Envelope<ChannelJoinPayload>): void;
227
+ /**
228
+ * Handle a CHANNEL_LEAVE message.
229
+ * Removes the member from the channel and notifies remaining members.
230
+ */
231
+ handleChannelLeave(connection: RoutableConnection, envelope: Envelope<ChannelLeavePayload>): void;
232
+ /**
233
+ * Route a channel message to all members except the sender.
234
+ */
235
+ routeChannelMessage(connection: RoutableConnection, envelope: Envelope<ChannelMessagePayload>): void;
236
+ /**
237
+ * Persist a channel message to storage.
238
+ */
239
+ private persistChannelMessage;
240
+ /**
241
+ * Get all members of a channel.
242
+ */
243
+ getChannelMembers(channel: string): string[];
244
+ /**
245
+ * Get all channels.
246
+ */
247
+ getChannels(): string[];
248
+ /**
249
+ * Get all channels a member is in.
250
+ */
251
+ getChannelsForMember(memberName: string): string[];
252
+ /**
253
+ * Check if a name belongs to a user (not an agent).
254
+ */
255
+ isUser(name: string): boolean;
256
+ /**
257
+ * Check if a name belongs to an agent (not a user).
258
+ */
259
+ isAgent(name: string): boolean;
260
+ /**
261
+ * Get list of connected user names (human users only).
262
+ */
263
+ getUsers(): string[];
264
+ /**
265
+ * Get a connection by name (checks both agents and users).
266
+ */
267
+ private getConnectionByName;
209
268
  }
210
269
  export {};
211
270
  //# sourceMappingURL=router.d.ts.map
@@ -24,6 +24,12 @@ export class Router {
24
24
  shadowsByPrimary = new Map();
25
25
  /** Reverse lookup: shadowAgent -> primaryAgent (for cleanup) */
26
26
  primaryByShadow = new Map();
27
+ /** Channel membership: channel -> Set of member names */
28
+ channels = new Map();
29
+ /** User entities (human users, not agents) */
30
+ users = new Map();
31
+ /** Reverse lookup: member name -> Set of channels they're in */
32
+ memberChannels = new Map();
27
33
  /** Default timeout for processing indicator (30 seconds) */
28
34
  static PROCESSING_TIMEOUT_MS = 30_000;
29
35
  /** Callback when processing state changes (for real-time dashboard updates) */
@@ -47,21 +53,34 @@ export class Router {
47
53
  register(connection) {
48
54
  this.connections.set(connection.id, connection);
49
55
  if (connection.agentName) {
50
- // Handle existing connection with same name (disconnect old)
51
- const existing = this.agents.get(connection.agentName);
52
- if (existing && existing.id !== connection.id) {
53
- existing.close();
54
- this.connections.delete(existing.id);
56
+ const isUser = connection.entityType === 'user';
57
+ if (isUser) {
58
+ // Handle existing user connection with same name (disconnect old)
59
+ const existingUser = this.users.get(connection.agentName);
60
+ if (existingUser && existingUser.id !== connection.id) {
61
+ existingUser.close();
62
+ this.connections.delete(existingUser.id);
63
+ }
64
+ this.users.set(connection.agentName, connection);
65
+ routerLog.info(`User registered: ${connection.agentName}`);
66
+ }
67
+ else {
68
+ // Handle existing agent connection with same name (disconnect old)
69
+ const existing = this.agents.get(connection.agentName);
70
+ if (existing && existing.id !== connection.id) {
71
+ existing.close();
72
+ this.connections.delete(existing.id);
73
+ }
74
+ this.agents.set(connection.agentName, connection);
75
+ this.registry?.registerOrUpdate({
76
+ name: connection.agentName,
77
+ cli: connection.cli,
78
+ program: connection.program,
79
+ model: connection.model,
80
+ task: connection.task,
81
+ workingDirectory: connection.workingDirectory,
82
+ });
55
83
  }
56
- this.agents.set(connection.agentName, connection);
57
- this.registry?.registerOrUpdate({
58
- name: connection.agentName,
59
- cli: connection.cli,
60
- program: connection.program,
61
- model: connection.model,
62
- task: connection.task,
63
- workingDirectory: connection.workingDirectory,
64
- });
65
84
  }
66
85
  }
67
86
  /**
@@ -70,14 +89,25 @@ export class Router {
70
89
  unregister(connection) {
71
90
  this.connections.delete(connection.id);
72
91
  if (connection.agentName) {
73
- const current = this.agents.get(connection.agentName);
74
- if (current?.id === connection.id) {
75
- this.agents.delete(connection.agentName);
92
+ const isUser = connection.entityType === 'user';
93
+ if (isUser) {
94
+ const currentUser = this.users.get(connection.agentName);
95
+ if (currentUser?.id === connection.id) {
96
+ this.users.delete(connection.agentName);
97
+ }
98
+ }
99
+ else {
100
+ const current = this.agents.get(connection.agentName);
101
+ if (current?.id === connection.id) {
102
+ this.agents.delete(connection.agentName);
103
+ }
76
104
  }
77
105
  // Remove from all subscriptions
78
106
  for (const subscribers of this.subscriptions.values()) {
79
107
  subscribers.delete(connection.agentName);
80
108
  }
109
+ // Remove from all channels and notify remaining members
110
+ this.removeFromAllChannels(connection.agentName);
81
111
  // Clean up shadow relationships
82
112
  this.unbindShadow(connection.agentName);
83
113
  // Clear processing state
@@ -85,6 +115,25 @@ export class Router {
85
115
  }
86
116
  this.clearPendingForConnection(connection.id);
87
117
  }
118
+ /**
119
+ * Remove a member from all channels they're in.
120
+ */
121
+ removeFromAllChannels(memberName) {
122
+ const memberChannelSet = this.memberChannels.get(memberName);
123
+ if (!memberChannelSet)
124
+ return;
125
+ for (const channelName of memberChannelSet) {
126
+ const members = this.channels.get(channelName);
127
+ if (members) {
128
+ members.delete(memberName);
129
+ // Clean up empty channels
130
+ if (members.size === 0) {
131
+ this.channels.delete(channelName);
132
+ }
133
+ }
134
+ }
135
+ this.memberChannels.delete(memberName);
136
+ }
88
137
  /**
89
138
  * Subscribe an agent to a topic.
90
139
  */
@@ -308,7 +357,8 @@ export class Router {
308
357
  * Send a direct message to a specific agent.
309
358
  */
310
359
  sendDirect(from, to, envelope) {
311
- const target = this.agents.get(to);
360
+ const target = this.agents.get(to) ?? this.users.get(to);
361
+ const isUserTarget = target?.entityType === 'user';
312
362
  // If agent not found locally, check if it's on a remote machine
313
363
  if (!target) {
314
364
  const remoteAgent = this.crossMachineHandler?.isRemoteAgent(to);
@@ -326,8 +376,10 @@ export class Router {
326
376
  if (sent) {
327
377
  this.trackDelivery(target, deliver);
328
378
  this.registry?.recordReceive(to);
329
- // Mark recipient as processing
330
- this.setProcessing(to, deliver.id);
379
+ // Only mark AI agents as processing; humans don't need processing indicators
380
+ if (!isUserTarget) {
381
+ this.setProcessing(to, deliver.id);
382
+ }
331
383
  }
332
384
  return sent;
333
385
  }
@@ -694,5 +746,214 @@ export class Router {
694
746
  }
695
747
  }
696
748
  }
749
+ // ==================== Channel Methods ====================
750
+ /**
751
+ * Handle a CHANNEL_JOIN message.
752
+ * Adds the member to the channel and notifies existing members.
753
+ */
754
+ handleChannelJoin(connection, envelope) {
755
+ const memberName = connection.agentName;
756
+ if (!memberName) {
757
+ routerLog.warn('CHANNEL_JOIN from connection without name');
758
+ return;
759
+ }
760
+ const channel = envelope.payload.channel;
761
+ // Get or create channel
762
+ let members = this.channels.get(channel);
763
+ if (!members) {
764
+ members = new Set();
765
+ this.channels.set(channel, members);
766
+ }
767
+ // Check if already a member
768
+ if (members.has(memberName)) {
769
+ routerLog.debug(`${memberName} already in ${channel}`);
770
+ return;
771
+ }
772
+ // Notify existing members about the new joiner
773
+ for (const existingMember of members) {
774
+ const memberConn = this.getConnectionByName(existingMember);
775
+ if (memberConn) {
776
+ const joinNotification = {
777
+ v: PROTOCOL_VERSION,
778
+ type: 'CHANNEL_JOIN',
779
+ id: uuid(),
780
+ ts: Date.now(),
781
+ from: memberName,
782
+ payload: envelope.payload,
783
+ };
784
+ memberConn.send(joinNotification);
785
+ }
786
+ }
787
+ // Add the new member
788
+ members.add(memberName);
789
+ // Track which channels this member is in
790
+ let memberChannelSet = this.memberChannels.get(memberName);
791
+ if (!memberChannelSet) {
792
+ memberChannelSet = new Set();
793
+ this.memberChannels.set(memberName, memberChannelSet);
794
+ }
795
+ memberChannelSet.add(channel);
796
+ routerLog.info(`${memberName} joined ${channel} (${members.size} members)`);
797
+ }
798
+ /**
799
+ * Handle a CHANNEL_LEAVE message.
800
+ * Removes the member from the channel and notifies remaining members.
801
+ */
802
+ handleChannelLeave(connection, envelope) {
803
+ const memberName = connection.agentName;
804
+ if (!memberName) {
805
+ routerLog.warn('CHANNEL_LEAVE from connection without name');
806
+ return;
807
+ }
808
+ const channel = envelope.payload.channel;
809
+ const members = this.channels.get(channel);
810
+ if (!members || !members.has(memberName)) {
811
+ routerLog.debug(`${memberName} not in ${channel}, ignoring leave`);
812
+ return;
813
+ }
814
+ // Remove from channel
815
+ members.delete(memberName);
816
+ // Remove from member's channel list
817
+ const memberChannelSet = this.memberChannels.get(memberName);
818
+ if (memberChannelSet) {
819
+ memberChannelSet.delete(channel);
820
+ if (memberChannelSet.size === 0) {
821
+ this.memberChannels.delete(memberName);
822
+ }
823
+ }
824
+ // Notify remaining members
825
+ for (const remainingMember of members) {
826
+ const memberConn = this.getConnectionByName(remainingMember);
827
+ if (memberConn) {
828
+ const leaveNotification = {
829
+ v: PROTOCOL_VERSION,
830
+ type: 'CHANNEL_LEAVE',
831
+ id: uuid(),
832
+ ts: Date.now(),
833
+ from: memberName,
834
+ payload: envelope.payload,
835
+ };
836
+ memberConn.send(leaveNotification);
837
+ }
838
+ }
839
+ // Clean up empty channels
840
+ if (members.size === 0) {
841
+ this.channels.delete(channel);
842
+ routerLog.debug(`Channel ${channel} deleted (empty)`);
843
+ }
844
+ routerLog.info(`${memberName} left ${channel}`);
845
+ }
846
+ /**
847
+ * Route a channel message to all members except the sender.
848
+ */
849
+ routeChannelMessage(connection, envelope) {
850
+ const senderName = connection.agentName;
851
+ if (!senderName) {
852
+ routerLog.warn('CHANNEL_MESSAGE from connection without name');
853
+ return;
854
+ }
855
+ const channel = envelope.payload.channel;
856
+ const members = this.channels.get(channel);
857
+ if (!members) {
858
+ routerLog.warn(`Message to non-existent channel ${channel}`);
859
+ return;
860
+ }
861
+ if (!members.has(senderName)) {
862
+ routerLog.warn(`${senderName} not a member of ${channel}`);
863
+ return;
864
+ }
865
+ // Route to all members except sender
866
+ for (const memberName of members) {
867
+ if (memberName === senderName)
868
+ continue;
869
+ const memberConn = this.getConnectionByName(memberName);
870
+ if (memberConn) {
871
+ const deliverEnvelope = {
872
+ v: PROTOCOL_VERSION,
873
+ type: 'CHANNEL_MESSAGE',
874
+ id: uuid(),
875
+ ts: Date.now(),
876
+ from: senderName,
877
+ payload: envelope.payload,
878
+ };
879
+ memberConn.send(deliverEnvelope);
880
+ }
881
+ }
882
+ // Persist channel message
883
+ this.persistChannelMessage(envelope, senderName);
884
+ routerLog.debug(`${senderName} -> ${channel}: ${envelope.payload.body.substring(0, 50)}`);
885
+ }
886
+ /**
887
+ * Persist a channel message to storage.
888
+ */
889
+ persistChannelMessage(envelope, from) {
890
+ if (!this.storage)
891
+ return;
892
+ this.storage.saveMessage({
893
+ id: envelope.id,
894
+ ts: envelope.ts,
895
+ from,
896
+ to: envelope.payload.channel, // Channel name as "to"
897
+ topic: undefined,
898
+ kind: 'message',
899
+ body: envelope.payload.body,
900
+ data: {
901
+ ...envelope.payload.data,
902
+ _isChannelMessage: true,
903
+ _channel: envelope.payload.channel,
904
+ _mentions: envelope.payload.mentions,
905
+ },
906
+ thread: envelope.payload.thread,
907
+ status: 'unread',
908
+ is_urgent: false,
909
+ is_broadcast: true, // Channel messages are effectively broadcasts
910
+ }).catch((err) => {
911
+ routerLog.error('Failed to persist channel message', { error: String(err) });
912
+ });
913
+ }
914
+ /**
915
+ * Get all members of a channel.
916
+ */
917
+ getChannelMembers(channel) {
918
+ const members = this.channels.get(channel);
919
+ return members ? Array.from(members) : [];
920
+ }
921
+ /**
922
+ * Get all channels.
923
+ */
924
+ getChannels() {
925
+ return Array.from(this.channels.keys());
926
+ }
927
+ /**
928
+ * Get all channels a member is in.
929
+ */
930
+ getChannelsForMember(memberName) {
931
+ const channels = this.memberChannels.get(memberName);
932
+ return channels ? Array.from(channels) : [];
933
+ }
934
+ /**
935
+ * Check if a name belongs to a user (not an agent).
936
+ */
937
+ isUser(name) {
938
+ return this.users.has(name);
939
+ }
940
+ /**
941
+ * Check if a name belongs to an agent (not a user).
942
+ */
943
+ isAgent(name) {
944
+ return this.agents.has(name);
945
+ }
946
+ /**
947
+ * Get list of connected user names (human users only).
948
+ */
949
+ getUsers() {
950
+ return Array.from(this.users.keys());
951
+ }
952
+ /**
953
+ * Get a connection by name (checks both agents and users).
954
+ */
955
+ getConnectionByName(name) {
956
+ return this.agents.get(name) ?? this.users.get(name);
957
+ }
697
958
  }
698
959
  //# sourceMappingURL=router.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * User Directory Service
3
+ *
4
+ * Manages per-user directories on workspace volumes for CLI credential storage.
5
+ * Each user gets their own home directory at /data/users/{userId}/ with
6
+ * provider-specific subdirectories for credentials.
7
+ *
8
+ * Structure:
9
+ * /data/
10
+ * └── users/
11
+ * ├── {userId1}/
12
+ * │ ├── .claude/
13
+ * │ │ └── .credentials.json
14
+ * │ ├── .codex/
15
+ * │ │ └── credentials.json
16
+ * │ └── .config/
17
+ * │ └── gcloud/
18
+ * │ └── application_default_credentials.json
19
+ * └── {userId2}/
20
+ * └── ...
21
+ */
22
+ /**
23
+ * Service for managing per-user directories on workspace volumes.
24
+ * Enables multi-user credential storage without conflicts.
25
+ */
26
+ export declare class UserDirectoryService {
27
+ private baseDir;
28
+ private usersDir;
29
+ /**
30
+ * Create a new UserDirectoryService.
31
+ * @param baseDir - Base data directory (e.g., /data)
32
+ */
33
+ constructor(baseDir: string);
34
+ /**
35
+ * Get the home directory path for a user.
36
+ * Creates the directory if it doesn't exist.
37
+ *
38
+ * @param userId - User ID (UUID or similar)
39
+ * @returns Absolute path to user's home directory
40
+ * @throws Error if userId is invalid
41
+ */
42
+ getUserHome(userId: string): string;
43
+ /**
44
+ * Ensure a provider's credential directory exists for a user.
45
+ *
46
+ * @param userId - User ID
47
+ * @param provider - Provider name (claude, codex, gemini, etc.)
48
+ * @returns Absolute path to provider directory
49
+ */
50
+ ensureProviderDir(userId: string, provider: string): string;
51
+ /**
52
+ * Initialize a complete user environment with all provider directories.
53
+ *
54
+ * @param userId - User ID
55
+ * @returns User's home directory path
56
+ */
57
+ initializeUserEnvironment(userId: string): string;
58
+ /**
59
+ * Get environment variables for spawning an agent with user-specific HOME.
60
+ *
61
+ * @param userId - User ID
62
+ * @returns Environment variables to merge with process.env
63
+ */
64
+ getUserEnvironment(userId: string): Record<string, string>;
65
+ /**
66
+ * List all user IDs that have directories.
67
+ *
68
+ * @returns Array of user IDs
69
+ */
70
+ listUsers(): string[];
71
+ /**
72
+ * Check if a user has an existing directory.
73
+ *
74
+ * @param userId - User ID
75
+ * @returns True if directory exists
76
+ */
77
+ hasUserDirectory(userId: string): boolean;
78
+ /**
79
+ * Get the path to a provider's credentials file for a user.
80
+ *
81
+ * @param userId - User ID
82
+ * @param provider - Provider name
83
+ * @returns Absolute path to credentials file
84
+ */
85
+ getProviderCredentialPath(userId: string, provider: string): string;
86
+ /**
87
+ * Validate a user ID to prevent path traversal and other issues.
88
+ *
89
+ * @param userId - User ID to validate
90
+ * @throws Error if userId is invalid
91
+ */
92
+ private validateUserId;
93
+ /**
94
+ * Ensure a directory exists, creating it recursively if needed.
95
+ */
96
+ private ensureDirectory;
97
+ }
98
+ /**
99
+ * Get the default data directory for user directories.
100
+ * Uses AGENT_RELAY_DATA_DIR if set, otherwise /data (for Fly.io volumes).
101
+ */
102
+ export declare function getDefaultDataDir(): string;
103
+ /**
104
+ * Get the singleton UserDirectoryService instance.
105
+ */
106
+ export declare function getUserDirectoryService(): UserDirectoryService;
107
+ /**
108
+ * Create a new UserDirectoryService for testing or custom paths.
109
+ */
110
+ export declare function createUserDirectoryService(baseDir: string): UserDirectoryService;
111
+ //# sourceMappingURL=user-directory.d.ts.map