agent-relay 1.3.0 → 1.3.2

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 (240) hide show
  1. package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
  2. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
  5. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
  6. package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
  7. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
  10. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
  11. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
  12. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
  13. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
  14. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
  15. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
  16. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
  17. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
  18. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
  19. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
  20. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
  21. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
  22. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
  23. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
  24. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
  25. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
  26. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
  27. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
  28. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
  29. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
  30. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
  31. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
  32. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
  33. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
  35. package/.trajectories/index.json +140 -1
  36. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  37. package/deploy/workspace/codex.config.toml +1 -1
  38. package/deploy/workspace/entrypoint.sh +20 -79
  39. package/deploy/workspace/gh-relay +156 -0
  40. package/deploy/workspace/git-credential-relay +5 -1
  41. package/dist/bridge/multi-project-client.js +13 -10
  42. package/dist/bridge/spawner.d.ts +2 -0
  43. package/dist/bridge/spawner.js +19 -1
  44. package/dist/bridge/types.d.ts +2 -0
  45. package/dist/cli/index.d.ts +1 -1
  46. package/dist/cli/index.js +115 -69
  47. package/dist/cloud/api/admin.js +16 -3
  48. package/dist/cloud/api/codex-auth-helper.js +28 -8
  49. package/dist/cloud/api/consensus.d.ts +13 -0
  50. package/dist/cloud/api/consensus.js +259 -0
  51. package/dist/cloud/api/daemons.js +205 -1
  52. package/dist/cloud/api/git.js +37 -7
  53. package/dist/cloud/api/onboarding.js +4 -1
  54. package/dist/cloud/api/provider-env.d.ts +5 -0
  55. package/dist/cloud/api/provider-env.js +27 -0
  56. package/dist/cloud/api/providers.js +2 -0
  57. package/dist/cloud/api/test-helpers.js +130 -0
  58. package/dist/cloud/api/workspaces.js +38 -3
  59. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  60. package/dist/cloud/db/bulk-ingest.js +268 -0
  61. package/dist/cloud/db/drizzle.d.ts +33 -0
  62. package/dist/cloud/db/drizzle.js +174 -2
  63. package/dist/cloud/db/index.d.ts +24 -5
  64. package/dist/cloud/db/index.js +19 -4
  65. package/dist/cloud/db/schema.d.ts +397 -3
  66. package/dist/cloud/db/schema.js +75 -1
  67. package/dist/cloud/provisioner/index.d.ts +8 -0
  68. package/dist/cloud/provisioner/index.js +256 -50
  69. package/dist/cloud/server.js +47 -3
  70. package/dist/cloud/services/index.d.ts +1 -0
  71. package/dist/cloud/services/index.js +2 -0
  72. package/dist/cloud/services/nango.d.ts +3 -4
  73. package/dist/cloud/services/nango.js +11 -33
  74. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  75. package/dist/cloud/services/workspace-keepalive.js +234 -0
  76. package/dist/config/relay-config.d.ts +23 -0
  77. package/dist/config/relay-config.js +23 -0
  78. package/dist/daemon/agent-manager.d.ts +20 -1
  79. package/dist/daemon/agent-manager.js +47 -0
  80. package/dist/daemon/agent-registry.js +4 -4
  81. package/dist/daemon/agent-signing.d.ts +158 -0
  82. package/dist/daemon/agent-signing.js +523 -0
  83. package/dist/daemon/api.js +18 -1
  84. package/dist/daemon/cli-auth.d.ts +4 -1
  85. package/dist/daemon/cli-auth.js +55 -11
  86. package/dist/daemon/cloud-sync.d.ts +47 -1
  87. package/dist/daemon/cloud-sync.js +152 -3
  88. package/dist/daemon/connection.d.ts +28 -0
  89. package/dist/daemon/connection.js +98 -15
  90. package/dist/daemon/consensus-integration.d.ts +167 -0
  91. package/dist/daemon/consensus-integration.js +371 -0
  92. package/dist/daemon/consensus.d.ts +271 -0
  93. package/dist/daemon/consensus.js +632 -0
  94. package/dist/daemon/delivery-tracker.d.ts +34 -0
  95. package/dist/daemon/delivery-tracker.js +104 -0
  96. package/dist/daemon/enhanced-features.d.ts +118 -0
  97. package/dist/daemon/enhanced-features.js +178 -0
  98. package/dist/daemon/index.d.ts +4 -0
  99. package/dist/daemon/index.js +5 -0
  100. package/dist/daemon/rate-limiter.d.ts +68 -0
  101. package/dist/daemon/rate-limiter.js +130 -0
  102. package/dist/daemon/router.d.ts +18 -11
  103. package/dist/daemon/router.js +55 -111
  104. package/dist/daemon/server.d.ts +13 -1
  105. package/dist/daemon/server.js +71 -9
  106. package/dist/daemon/sync-queue.d.ts +116 -0
  107. package/dist/daemon/sync-queue.js +361 -0
  108. package/dist/health-worker-manager.d.ts +62 -0
  109. package/dist/health-worker-manager.js +144 -0
  110. package/dist/health-worker.d.ts +9 -0
  111. package/dist/health-worker.js +79 -0
  112. package/dist/index.d.ts +2 -1
  113. package/dist/index.js +5 -1
  114. package/dist/memory/context-compaction.d.ts +156 -0
  115. package/dist/memory/context-compaction.js +453 -0
  116. package/dist/memory/index.d.ts +1 -0
  117. package/dist/memory/index.js +1 -0
  118. package/dist/protocol/channels.js +4 -4
  119. package/dist/protocol/framing.d.ts +72 -10
  120. package/dist/protocol/framing.js +194 -25
  121. package/dist/storage/adapter.d.ts +8 -1
  122. package/dist/storage/adapter.js +11 -0
  123. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  124. package/dist/storage/batched-sqlite-adapter.js +183 -0
  125. package/dist/storage/dead-letter-queue.d.ts +196 -0
  126. package/dist/storage/dead-letter-queue.js +427 -0
  127. package/dist/storage/dlq-adapter.d.ts +195 -0
  128. package/dist/storage/dlq-adapter.js +664 -0
  129. package/dist/trajectory/config.d.ts +32 -14
  130. package/dist/trajectory/config.js +38 -16
  131. package/dist/trajectory/integration.js +217 -64
  132. package/dist/utils/git-remote.d.ts +47 -0
  133. package/dist/utils/git-remote.js +125 -0
  134. package/dist/utils/id-generator.d.ts +35 -0
  135. package/dist/utils/id-generator.js +60 -0
  136. package/dist/utils/index.d.ts +1 -0
  137. package/dist/utils/index.js +1 -0
  138. package/dist/utils/precompiled-patterns.d.ts +110 -0
  139. package/dist/utils/precompiled-patterns.js +322 -0
  140. package/dist/wrapper/auth-detection.js +1 -1
  141. package/dist/wrapper/base-wrapper.d.ts +36 -0
  142. package/dist/wrapper/base-wrapper.js +48 -2
  143. package/dist/wrapper/client.d.ts +14 -4
  144. package/dist/wrapper/client.js +84 -31
  145. package/dist/wrapper/idle-detector.d.ts +102 -0
  146. package/dist/wrapper/idle-detector.js +279 -0
  147. package/dist/wrapper/parser.d.ts +4 -0
  148. package/dist/wrapper/parser.js +19 -1
  149. package/dist/wrapper/pty-wrapper.d.ts +7 -1
  150. package/dist/wrapper/pty-wrapper.js +51 -27
  151. package/dist/wrapper/tmux-wrapper.d.ts +12 -1
  152. package/dist/wrapper/tmux-wrapper.js +65 -17
  153. package/package.json +5 -5
  154. package/scripts/run-migrations.js +43 -0
  155. package/scripts/verify-schema.js +134 -0
  156. package/tests/benchmarks/protocol.bench.ts +310 -0
  157. package/dist/dashboard/out/404.html +0 -1
  158. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_buildManifest.js +0 -1
  159. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +0 -1
  160. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  161. package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +0 -2
  162. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
  163. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
  164. package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +0 -1
  165. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +0 -1
  166. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +0 -1
  167. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +0 -1
  168. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +0 -1
  169. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +0 -1
  170. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +0 -1
  171. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +0 -1
  172. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +0 -1
  173. package/dist/dashboard/out/_next/static/chunks/app/history/page-8c8bed33beb2bf1c.js +0 -1
  174. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  175. package/dist/dashboard/out/_next/static/chunks/app/login/page-16f3b49e55b1e0ed.js +0 -1
  176. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +0 -1
  177. package/dist/dashboard/out/_next/static/chunks/app/page-4a5938c18a11a654.js +0 -1
  178. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +0 -1
  179. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +0 -1
  180. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/app/signup/page-547dd0ca55ecd0ba.js +0 -1
  182. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
  183. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/main-2ee6beb2ae96d210.js +0 -1
  186. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +0 -1
  187. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
  188. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
  189. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  190. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
  191. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +0 -1
  192. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +0 -1
  193. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  194. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  195. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  196. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  197. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  198. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
  199. package/dist/dashboard/out/alt-logos/logo.svg +0 -38
  200. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  201. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  202. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  203. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  204. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  205. package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
  206. package/dist/dashboard/out/app/onboarding.html +0 -1
  207. package/dist/dashboard/out/app/onboarding.txt +0 -7
  208. package/dist/dashboard/out/app.html +0 -1
  209. package/dist/dashboard/out/app.txt +0 -7
  210. package/dist/dashboard/out/apple-icon.png +0 -0
  211. package/dist/dashboard/out/connect-repos.html +0 -1
  212. package/dist/dashboard/out/connect-repos.txt +0 -7
  213. package/dist/dashboard/out/history.html +0 -1
  214. package/dist/dashboard/out/history.txt +0 -7
  215. package/dist/dashboard/out/index.html +0 -1
  216. package/dist/dashboard/out/index.txt +0 -7
  217. package/dist/dashboard/out/login.html +0 -6
  218. package/dist/dashboard/out/login.txt +0 -7
  219. package/dist/dashboard/out/metrics.html +0 -1
  220. package/dist/dashboard/out/metrics.txt +0 -7
  221. package/dist/dashboard/out/pricing.html +0 -13
  222. package/dist/dashboard/out/pricing.txt +0 -7
  223. package/dist/dashboard/out/providers/setup/claude.html +0 -1
  224. package/dist/dashboard/out/providers/setup/claude.txt +0 -8
  225. package/dist/dashboard/out/providers/setup/codex.html +0 -1
  226. package/dist/dashboard/out/providers/setup/codex.txt +0 -8
  227. package/dist/dashboard/out/providers.html +0 -1
  228. package/dist/dashboard/out/providers.txt +0 -7
  229. package/dist/dashboard/out/signup.html +0 -6
  230. package/dist/dashboard/out/signup.txt +0 -7
  231. package/dist/dashboard-server/metrics.d.ts +0 -105
  232. package/dist/dashboard-server/metrics.js +0 -193
  233. package/dist/dashboard-server/needs-attention.d.ts +0 -24
  234. package/dist/dashboard-server/needs-attention.js +0 -78
  235. package/dist/dashboard-server/server.d.ts +0 -15
  236. package/dist/dashboard-server/server.js +0 -3776
  237. package/dist/dashboard-server/start.d.ts +0 -6
  238. package/dist/dashboard-server/start.js +0 -13
  239. package/dist/dashboard-server/user-bridge.d.ts +0 -103
  240. package/dist/dashboard-server/user-bridge.js +0 -189
@@ -23,6 +23,8 @@ interface WorkerMeta {
23
23
  task: string;
24
24
  /** Optional team name this agent belongs to */
25
25
  team?: string;
26
+ /** Optional user ID for per-user credential scoping */
27
+ userId?: string;
26
28
  spawnedAt: number;
27
29
  pid?: number;
28
30
  logFile?: string;
@@ -12,6 +12,7 @@ import { PtyWrapper } from '../wrapper/pty-wrapper.js';
12
12
  import { selectShadowCli } from './shadow-cli.js';
13
13
  import { AgentPolicyService } from '../policy/agent-policy.js';
14
14
  import { buildClaudeArgs } from '../utils/agent-config.js';
15
+ import { getUserDirectoryService } from '../daemon/user-directory.js';
15
16
  /**
16
17
  * Get a minimal relay reminder.
17
18
  * Agents already have full relay docs via CLAUDE.md - this is just a brief reminder.
@@ -150,7 +151,7 @@ export class AgentSpawner {
150
151
  * Spawn a new worker agent using node-pty
151
152
  */
152
153
  async spawn(request) {
153
- const { name, cli, task, team, spawnerName } = request;
154
+ const { name, cli, task, team, spawnerName, userId } = request;
154
155
  const debug = process.env.DEBUG_SPAWN === '1';
155
156
  // Check if worker already exists
156
157
  if (this.activeWorkers.has(name)) {
@@ -219,6 +220,19 @@ export class AgentSpawner {
219
220
  const agentCwd = request.cwd || this.projectRoot;
220
221
  // Log whether nested spawning will be enabled for this agent
221
222
  console.log(`[spawner] Spawning ${name}: dashboardPort=${this.dashboardPort || 'none'} (${this.dashboardPort ? 'nested spawns enabled' : 'nested spawns disabled'})`);
223
+ let userEnv;
224
+ if (userId) {
225
+ try {
226
+ const userDirService = getUserDirectoryService();
227
+ userEnv = userDirService.getUserEnvironment(userId);
228
+ }
229
+ catch (err) {
230
+ console.warn('[spawner] Failed to resolve user environment, using default', {
231
+ userId,
232
+ error: err instanceof Error ? err.message : String(err),
233
+ });
234
+ }
235
+ }
222
236
  const ptyConfig = {
223
237
  name,
224
238
  command,
@@ -227,6 +241,7 @@ export class AgentSpawner {
227
241
  cwd: agentCwd,
228
242
  logsDir: this.logsDir,
229
243
  dashboardPort: this.dashboardPort,
244
+ env: userEnv,
230
245
  // Interactive mode - disables auto-accept for auth setup flows
231
246
  interactive: request.interactive,
232
247
  // Shadow agent configuration
@@ -242,6 +257,7 @@ export class AgentSpawner {
242
257
  cli: workerCli,
243
258
  task: workerTask,
244
259
  // Nested spawns don't inherit team - they're flat by default
260
+ userId,
245
261
  });
246
262
  },
247
263
  onRelease: this.dashboardPort ? undefined : async (workerName) => {
@@ -386,6 +402,7 @@ export class AgentSpawner {
386
402
  cli,
387
403
  task,
388
404
  team,
405
+ userId,
389
406
  spawnedAt: Date.now(),
390
407
  pid: pty.pid,
391
408
  pty,
@@ -674,6 +691,7 @@ export class AgentSpawner {
674
691
  cli: w.cli,
675
692
  task: w.task,
676
693
  team: w.team,
694
+ userId: w.userId,
677
695
  spawnedAt: w.spawnedAt,
678
696
  pid: w.pid,
679
697
  logFile: w.logFile,
@@ -53,6 +53,8 @@ export interface SpawnRequest {
53
53
  shadowTriggers?: Array<'SESSION_END' | 'CODE_WRITTEN' | 'REVIEW_REQUEST' | 'EXPLICIT_ASK' | 'ALL_MESSAGES'>;
54
54
  /** When the shadow should speak (default: ['EXPLICIT_ASK']) */
55
55
  shadowSpeakOn?: Array<'SESSION_END' | 'CODE_WRITTEN' | 'REVIEW_REQUEST' | 'EXPLICIT_ASK' | 'ALL_MESSAGES'>;
56
+ /** User ID for per-user credential storage in shared workspaces */
57
+ userId?: string;
56
58
  }
57
59
  /** Policy decision details */
58
60
  export interface PolicyDecision {
@@ -5,7 +5,7 @@
5
5
  * Commands:
6
6
  * relay <cmd> - Wrap agent with real-time messaging (default)
7
7
  * relay -n Name cmd - Wrap with specific agent name
8
- * relay up - Start daemon + dashboard
8
+ * relay up - Start daemon
9
9
  * relay read <id> - Read full message by ID
10
10
  * relay agents - List connected agents
11
11
  * relay who - Show currently active agents
package/dist/cli/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Commands:
6
6
  * relay <cmd> - Wrap agent with real-time messaging (default)
7
7
  * relay -n Name cmd - Wrap with specific agent name
8
- * relay up - Start daemon + dashboard
8
+ * relay up - Start daemon
9
9
  * relay read <id> - Read full message by ID
10
10
  * relay agents - List connected agents
11
11
  * relay who - Show currently active agents
@@ -26,7 +26,6 @@ import { promisify } from 'node:util';
26
26
  import { exec } from 'node:child_process';
27
27
  import { fileURLToPath } from 'node:url';
28
28
  dotenvConfig();
29
- const DEFAULT_DASHBOARD_PORT = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
30
29
  // Read version from package.json
31
30
  const __filename = fileURLToPath(import.meta.url);
32
31
  const __dirname = path.dirname(__filename);
@@ -55,7 +54,6 @@ program
55
54
  .option('-n, --name <name>', 'Agent name (auto-generated if not set)')
56
55
  .option('-q, --quiet', 'Disable debug output', false)
57
56
  .option('--prefix <pattern>', 'Relay prefix pattern (default: ->relay:)')
58
- .option('--dashboard-port <port>', 'Dashboard port for spawn/release API (auto-detected if not set)')
59
57
  .option('--shadow <name>', 'Spawn a shadow agent with this name that monitors the primary')
60
58
  .option('--shadow-role <role>', 'Shadow role: reviewer, auditor, or triggers (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
61
59
  .argument('[command...]', 'Command to wrap (e.g., claude)')
@@ -86,31 +84,8 @@ program
86
84
  }
87
85
  const { TmuxWrapper } = await import('../wrapper/tmux-wrapper.js');
88
86
  const { AgentSpawner } = await import('../bridge/spawner.js');
89
- // Determine dashboard port for spawn/release API
90
- // Priority: CLI flag > env var > auto-detect default port
91
- let dashboardPort;
92
- if (options.dashboardPort) {
93
- dashboardPort = parseInt(options.dashboardPort, 10);
94
- }
95
- else {
96
- // Try to detect if dashboard is running at default port
97
- const defaultPort = parseInt(DEFAULT_DASHBOARD_PORT, 10);
98
- try {
99
- const response = await fetch(`http://localhost:${defaultPort}/api/status`, {
100
- method: 'GET',
101
- signal: AbortSignal.timeout(500), // Quick timeout for detection
102
- });
103
- if (response.ok) {
104
- dashboardPort = defaultPort;
105
- console.error(`Dashboard detected: http://localhost:${dashboardPort}`);
106
- }
107
- }
108
- catch {
109
- // Dashboard not running - spawn/release will use fallback callbacks
110
- }
111
- }
112
- // Create spawner as fallback for direct spawn (if dashboard API not available)
113
- const spawner = new AgentSpawner(paths.projectRoot, undefined, dashboardPort);
87
+ // Create spawner for spawn/release callbacks
88
+ const spawner = new AgentSpawner(paths.projectRoot);
114
89
  const wrapper = new TmuxWrapper({
115
90
  name: agentName,
116
91
  command: mainCommand,
@@ -120,9 +95,7 @@ program
120
95
  relayPrefix: options.prefix,
121
96
  useInbox: true,
122
97
  inboxDir: paths.dataDir, // Use the project-specific data directory for the inbox
123
- // Use dashboard API for spawn/release when available (preferred - works from any context)
124
- dashboardPort,
125
- // Wire up spawn/release callbacks as fallback (if no dashboardPort)
98
+ // Wire up spawn/release callbacks
126
99
  onSpawn: async (workerName, workerCli, task) => {
127
100
  console.error(`[${agentName}] Spawning ${workerName} (${workerCli})...`);
128
101
  const result = await spawner.spawn({
@@ -226,12 +199,10 @@ program
226
199
  }
227
200
  }
228
201
  });
229
- // up - Start daemon + dashboard
202
+ // up - Start daemon
230
203
  program
231
204
  .command('up')
232
- .description('Start daemon + dashboard')
233
- .option('--no-dashboard', 'Disable web dashboard')
234
- .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
205
+ .description('Start daemon')
235
206
  .option('--spawn', 'Force spawn all agents from teams.json')
236
207
  .option('--no-spawn', 'Do not auto-spawn agents (just start daemon)')
237
208
  .option('--watch', 'Auto-restart daemon on crash (supervisor mode)')
@@ -248,10 +219,6 @@ program
248
219
  const startDaemon = () => {
249
220
  // Build args without --watch to prevent infinite recursion
250
221
  const args = ['up'];
251
- if (options.dashboard === false)
252
- args.push('--no-dashboard');
253
- if (options.port)
254
- args.push('--port', options.port);
255
222
  if (options.spawn === true)
256
223
  args.push('--spawn');
257
224
  if (options.spawn === false)
@@ -363,28 +330,6 @@ program
363
330
  try {
364
331
  await daemon.start();
365
332
  console.log('Daemon started.');
366
- let dashboardPort;
367
- // Dashboard starts by default (use --no-dashboard to disable)
368
- if (options.dashboard !== false) {
369
- const port = parseInt(options.port, 10);
370
- const { startDashboard } = await import('../dashboard-server/server.js');
371
- dashboardPort = await startDashboard({
372
- port,
373
- dataDir: paths.dataDir,
374
- teamDir: paths.teamDir,
375
- dbPath,
376
- enableSpawner: true,
377
- projectRoot: paths.projectRoot,
378
- });
379
- console.log(`Dashboard: http://localhost:${dashboardPort}`);
380
- // Hook daemon log output to dashboard WebSocket
381
- daemon.onLogOutput = (agentName, data, _timestamp) => {
382
- const broadcast = global.__broadcastLogOutput;
383
- if (broadcast) {
384
- broadcast(agentName, data);
385
- }
386
- };
387
- }
388
333
  // Determine if we should auto-spawn agents
389
334
  // --spawn: force spawn
390
335
  // --no-spawn: never spawn
@@ -397,7 +342,7 @@ program
397
342
  if (shouldSpawn && teamsConfig && teamsConfig.agents.length > 0) {
398
343
  console.log('');
399
344
  console.log('Auto-spawning agents from teams.json...');
400
- spawner = new AgentSpawner(paths.projectRoot, undefined, dashboardPort);
345
+ spawner = new AgentSpawner(paths.projectRoot);
401
346
  for (const agent of teamsConfig.agents) {
402
347
  console.log(` Spawning ${agent.name} (${agent.cli})...`);
403
348
  const result = await spawner.spawn({
@@ -1341,14 +1286,113 @@ program
1341
1286
  }
1342
1287
  }
1343
1288
  });
1289
+ // spawn - Spawn an agent via API (works from any context, no tmux required)
1290
+ program
1291
+ .command('spawn', { hidden: true })
1292
+ .description('Spawn an agent via dashboard API (no tmux required, works in containers)')
1293
+ .argument('<name>', 'Agent name')
1294
+ .argument('<cli>', 'CLI to use (claude, codex, gemini, etc.)')
1295
+ .argument('[task]', 'Task description (can also be piped via stdin)')
1296
+ .option('--port <port>', 'Dashboard port', '3888')
1297
+ .option('--team <team>', 'Team name for the agent')
1298
+ .option('--spawner <name>', 'Name of the agent requesting the spawn (for policy enforcement)')
1299
+ .option('--interactive', 'Disable auto-accept of permission prompts (for auth setup flows)')
1300
+ .option('--cwd <path>', 'Working directory for the agent')
1301
+ .option('--shadow-mode <mode>', 'Shadow execution mode: subagent or process')
1302
+ .option('--shadow-of <name>', 'Primary agent to shadow (if this agent is a shadow)')
1303
+ .option('--shadow-agent <profile>', 'Shadow agent profile to use')
1304
+ .option('--shadow-triggers <triggers>', 'When to trigger shadow (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
1305
+ .option('--shadow-speak-on <triggers>', 'When shadow should speak (comma-separated, same values as --shadow-triggers)')
1306
+ .action(async (name, cli, task, options) => {
1307
+ const port = options.port || '3888';
1308
+ // Read task from stdin if not provided as argument
1309
+ let finalTask = task;
1310
+ if (!finalTask && !process.stdin.isTTY) {
1311
+ const chunks = [];
1312
+ for await (const chunk of process.stdin) {
1313
+ chunks.push(chunk);
1314
+ }
1315
+ finalTask = Buffer.concat(chunks).toString('utf-8').trim();
1316
+ }
1317
+ if (!finalTask) {
1318
+ console.error('Error: Task description required (as argument or via stdin)');
1319
+ process.exit(1);
1320
+ }
1321
+ // Validate shadow mode if provided
1322
+ if (options.shadowMode && !['subagent', 'process'].includes(options.shadowMode)) {
1323
+ console.error('Error: --shadow-mode must be "subagent" or "process"');
1324
+ process.exit(1);
1325
+ }
1326
+ // Parse comma-separated trigger lists
1327
+ const parseTriggers = (value) => {
1328
+ if (!value)
1329
+ return undefined;
1330
+ const validTriggers = ['SESSION_END', 'CODE_WRITTEN', 'REVIEW_REQUEST', 'EXPLICIT_ASK', 'ALL_MESSAGES'];
1331
+ const triggers = value.split(',').map(t => t.trim().toUpperCase());
1332
+ const invalid = triggers.filter(t => !validTriggers.includes(t));
1333
+ if (invalid.length > 0) {
1334
+ console.error(`Error: Invalid triggers: ${invalid.join(', ')}`);
1335
+ console.error(`Valid triggers: ${validTriggers.join(', ')}`);
1336
+ process.exit(1);
1337
+ }
1338
+ return triggers;
1339
+ };
1340
+ try {
1341
+ // Build spawn request using the SpawnRequest type for consistency
1342
+ const spawnRequest = {
1343
+ name,
1344
+ cli,
1345
+ task: finalTask,
1346
+ team: options.team,
1347
+ spawnerName: options.spawner,
1348
+ interactive: options.interactive,
1349
+ cwd: options.cwd,
1350
+ shadowMode: options.shadowMode,
1351
+ shadowOf: options.shadowOf,
1352
+ shadowAgent: options.shadowAgent,
1353
+ shadowTriggers: parseTriggers(options.shadowTriggers),
1354
+ shadowSpeakOn: parseTriggers(options.shadowSpeakOn),
1355
+ };
1356
+ const response = await fetch(`http://localhost:${port}/api/spawn`, {
1357
+ method: 'POST',
1358
+ headers: { 'Content-Type': 'application/json' },
1359
+ body: JSON.stringify(spawnRequest),
1360
+ });
1361
+ const result = await response.json();
1362
+ if (result.success) {
1363
+ console.log(`Spawned agent: ${name} (pid: ${result.pid})`);
1364
+ process.exit(0);
1365
+ }
1366
+ else {
1367
+ if (result.policyDecision) {
1368
+ console.error(`Policy denied spawn: ${result.policyDecision.reason}`);
1369
+ console.error(`Policy source: ${result.policyDecision.policySource}`);
1370
+ }
1371
+ else {
1372
+ console.error(`Failed to spawn ${name}: ${result.error || 'Unknown error'}`);
1373
+ }
1374
+ process.exit(1);
1375
+ }
1376
+ }
1377
+ catch (err) {
1378
+ if (err.code === 'ECONNREFUSED') {
1379
+ console.error(`Cannot connect to dashboard at port ${port}. Is the daemon running?`);
1380
+ console.log(`Run 'agent-relay up' to start the daemon.`);
1381
+ }
1382
+ else {
1383
+ console.error(`Failed to spawn ${name}: ${err.message}`);
1384
+ }
1385
+ process.exit(1);
1386
+ }
1387
+ });
1344
1388
  // release - Release a spawned agent via API (works from any context, no terminal required)
1345
1389
  program
1346
1390
  .command('release')
1347
1391
  .description('Release a spawned agent via API (no terminal required)')
1348
1392
  .argument('<name>', 'Agent name to release')
1349
- .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1393
+ .option('--port <port>', 'Dashboard port', '3888')
1350
1394
  .action(async (name, options) => {
1351
- const port = options.port || DEFAULT_DASHBOARD_PORT;
1395
+ const port = options.port || '3888';
1352
1396
  try {
1353
1397
  const response = await fetch(`http://localhost:${port}/api/spawned/${encodeURIComponent(name)}`, {
1354
1398
  method: 'DELETE',
@@ -1439,7 +1483,7 @@ cloudCommand
1439
1483
  .command('link')
1440
1484
  .description('Link this machine to your Agent Relay Cloud account')
1441
1485
  .option('--name <name>', 'Name for this machine')
1442
- .option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://api.agent-relay.com')
1486
+ .option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://agent-relay.com')
1443
1487
  .action(async (options) => {
1444
1488
  const os = await import('node:os');
1445
1489
  const crypto = await import('node:crypto');
@@ -1720,6 +1764,8 @@ program
1720
1764
  stdio: 'inherit',
1721
1765
  env: {
1722
1766
  ...process.env,
1767
+ // Trajectory env vars override parent shell settings
1768
+ // This ensures config-based TRAJECTORIES_DATA_DIR takes precedence
1723
1769
  TRAJECTORIES_PROJECT: paths.projectId,
1724
1770
  TRAJECTORIES_DATA_DIR: trajectoriesDir,
1725
1771
  },
@@ -1914,12 +1960,12 @@ program
1914
1960
  .command('metrics')
1915
1961
  .description('Show agent memory metrics and resource usage')
1916
1962
  .option('--agent <name>', 'Show metrics for specific agent')
1917
- .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1963
+ .option('--port <port>', 'Dashboard port', '3888')
1918
1964
  .option('--json', 'Output as JSON')
1919
1965
  .option('--watch', 'Continuously update metrics')
1920
1966
  .option('--interval <ms>', 'Update interval for watch mode', '5000')
1921
1967
  .action(async (options) => {
1922
- const port = options.port || DEFAULT_DASHBOARD_PORT;
1968
+ const port = options.port || '3888';
1923
1969
  const fetchMetrics = async () => {
1924
1970
  try {
1925
1971
  const response = await fetch(`http://localhost:${port}/api/metrics/agents`);
@@ -2033,12 +2079,12 @@ program
2033
2079
  program
2034
2080
  .command('health')
2035
2081
  .description('Show system health, crash insights, and recommendations')
2036
- .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
2082
+ .option('--port <port>', 'Dashboard port', '3888')
2037
2083
  .option('--json', 'Output as JSON')
2038
2084
  .option('--crashes', 'Show recent crash history')
2039
2085
  .option('--alerts', 'Show unacknowledged alerts')
2040
2086
  .action(async (options) => {
2041
- const port = options.port || DEFAULT_DASHBOARD_PORT;
2087
+ const port = options.port || '3888';
2042
2088
  try {
2043
2089
  const response = await fetch(`http://localhost:${port}/api/metrics/health`);
2044
2090
  if (!response.ok) {
@@ -43,7 +43,7 @@ adminRouter.use(requireAdminAuth);
43
43
  * - batchSize?: Number of concurrent updates (default: 5)
44
44
  *
45
45
  * Response:
46
- * - summary: { total, updated, pendingRestart, skippedActiveAgents, skippedNotRunning, errors }
46
+ * - summary: { total, updated, pendingRestart, skippedActiveAgents, skippedVerificationFailed, skippedNotRunning, errors }
47
47
  * - results: Array of per-workspace results
48
48
  */
49
49
  adminRouter.post('/workspaces/update-image', async (req, res) => {
@@ -159,7 +159,9 @@ adminRouter.get('/workspaces/:id/agents', async (req, res) => {
159
159
  return;
160
160
  }
161
161
  try {
162
- const response = await fetch(`${baseUrl}/api/agents`, {
162
+ // Use /api/data endpoint which returns { agents: [...], ... }
163
+ // Note: /api/agents doesn't exist on the workspace dashboard-server
164
+ const response = await fetch(`${baseUrl}/api/data`, {
163
165
  method: 'GET',
164
166
  headers: { 'Accept': 'application/json' },
165
167
  signal: AbortSignal.timeout(10_000),
@@ -170,7 +172,18 @@ adminRouter.get('/workspaces/:id/agents', async (req, res) => {
170
172
  }
171
173
  const data = await response.json();
172
174
  const agents = data.agents || [];
173
- const activeAgents = agents.filter(a => a.status === 'running' || a.activityState === 'active' || a.activityState === 'idle');
175
+ const activeAgents = agents.filter(a => {
176
+ const status = (a.status ?? '').toLowerCase();
177
+ const activityState = (a.activityState ?? '').toLowerCase();
178
+ const isProcessing = a.isProcessing === true;
179
+ if (activityState === 'active' || activityState === 'idle')
180
+ return true;
181
+ if (status && status !== 'disconnected' && status !== 'offline')
182
+ return true;
183
+ if (isProcessing)
184
+ return true;
185
+ return false;
186
+ });
174
187
  res.json({
175
188
  hasActiveAgents: activeAgents.length > 0,
176
189
  agentCount: activeAgents.length,
@@ -19,6 +19,7 @@ import { requireAuth } from './auth.js';
19
19
  import { db } from '../db/index.js';
20
20
  import { deriveSshPassword } from '../services/ssh-security.js';
21
21
  export const codexAuthHelperRouter = Router();
22
+ const WORKSPACE_SSH_PORT = 3022;
22
23
  const pendingAuthSessions = new Map();
23
24
  const pendingCliTokens = new Map();
24
25
  // Clean up old sessions every minute
@@ -201,18 +202,18 @@ codexAuthHelperRouter.get('/tunnel-info/:workspaceId', async (req, res) => {
201
202
  const host = url.hostname;
202
203
  const apiPort = parseInt(url.port, 10) || 80;
203
204
  // SSH connection info varies by environment:
204
- // - Fly.io: Use public fly.dev hostname with port 2222 (exposed via TCP service)
205
+ // - Fly.io: Use public fly.dev hostname with port 3022 (exposed via TCP service)
205
206
  // - Local Docker: Use localhost with derived SSH port (22000 + apiPort - 3000)
206
207
  const isOnFly = !!process.env.FLY_APP_NAME;
207
208
  const isLocalDocker = (host === 'localhost' || host === '127.0.0.1') && apiPort >= 3000;
208
209
  let sshHost;
209
210
  let sshPort;
210
211
  if (isOnFly) {
211
- // Fly.io public hostname - SSH is exposed as a public TCP service on port 2222
212
- // Users can SSH directly from their machine to {app}.fly.dev:2222
212
+ // Fly.io public hostname - SSH is exposed as a public TCP service on port 3022
213
+ // Users can SSH directly from their machine to {app}.fly.dev:3022
213
214
  const appName = `ar-${workspace.id.substring(0, 8)}`;
214
215
  sshHost = `${appName}.fly.dev`;
215
- sshPort = 2222;
216
+ sshPort = WORKSPACE_SSH_PORT;
216
217
  }
217
218
  else if (isLocalDocker) {
218
219
  // Local Docker: SSH port is derived from API port
@@ -223,7 +224,7 @@ codexAuthHelperRouter.get('/tunnel-info/:workspaceId', async (req, res) => {
223
224
  else {
224
225
  // Default fallback
225
226
  sshHost = host;
226
- sshPort = 2222;
227
+ sshPort = WORKSPACE_SSH_PORT;
227
228
  }
228
229
  // SSH password is derived per-workspace for security
229
230
  // Each workspace gets a unique password based on its ID + secret salt
@@ -288,18 +289,37 @@ codexAuthHelperRouter.get('/auth-status/:workspaceId', async (req, res) => {
288
289
  if (!workspace.publicUrl) {
289
290
  return res.status(400).json({ error: 'Workspace URL not available' });
290
291
  }
291
- // Check with workspace daemon if Codex is authenticated
292
- const response = await fetch(`${workspace.publicUrl}/auth/cli/openai/check`, {
292
+ // Check with workspace daemon if Codex is authenticated for this specific user
293
+ // Pass userId to enable per-user credential checking (multiple users can share a workspace)
294
+ const checkUrl = new URL(`${workspace.publicUrl}/auth/cli/openai/check`);
295
+ checkUrl.searchParams.set('userId', userId);
296
+ const response = await fetch(checkUrl.toString(), {
293
297
  method: 'GET',
294
298
  signal: AbortSignal.timeout(5000),
295
299
  });
296
300
  if (response.ok) {
297
301
  const data = await response.json();
302
+ // When authentication is detected, mark the provider as connected in the database
303
+ // This ensures the dashboard shows correct per-user connection status
304
+ if (data.authenticated && userId) {
305
+ try {
306
+ await db.credentials.upsert({
307
+ userId,
308
+ provider: 'codex', // Codex provider for OpenAI
309
+ scopes: [],
310
+ });
311
+ console.log(`[codex-helper] Marked codex as connected for user ${userId}`);
312
+ }
313
+ catch (dbError) {
314
+ console.error('[codex-helper] Failed to mark provider as connected:', dbError);
315
+ // Don't fail the request if DB update fails
316
+ }
317
+ }
298
318
  return res.json({ authenticated: data.authenticated });
299
319
  }
300
320
  res.json({ authenticated: false });
301
321
  }
302
- catch (error) {
322
+ catch (_error) {
303
323
  // Workspace might not be reachable, return false
304
324
  res.json({ authenticated: false });
305
325
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Consensus API Routes (Read-Only)
3
+ *
4
+ * Provides API endpoints for observing multi-agent consensus decisions.
5
+ * The dashboard is read-only - agents handle all consensus activity via relay messages.
6
+ *
7
+ * Architecture:
8
+ * - Agents create proposals and vote via ->relay:_consensus messages
9
+ * - The daemon processes these and syncs state to cloud via /sync endpoint
10
+ * - Dashboard reads consensus state for display only
11
+ */
12
+ export declare const consensusRouter: import("express-serve-static-core").Router;
13
+ //# sourceMappingURL=consensus.d.ts.map