agent-relay 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
- package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
- package/.trajectories/index.json +140 -1
- package/README.md +23 -9
- package/TRAIL_GIT_AUTH_FIX.md +113 -0
- package/deploy/workspace/codex.config.toml +1 -1
- package/deploy/workspace/entrypoint.sh +20 -79
- package/deploy/workspace/gh-relay +156 -0
- package/deploy/workspace/git-credential-relay +5 -1
- package/dist/bridge/multi-project-client.js +13 -10
- package/dist/bridge/spawner.d.ts +2 -0
- package/dist/bridge/spawner.js +58 -76
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.d.ts +8 -6
- package/dist/cli/index.js +297 -30
- package/dist/cloud/api/admin.js +16 -3
- package/dist/cloud/api/codex-auth-helper.js +28 -8
- package/dist/cloud/api/consensus.d.ts +13 -0
- package/dist/cloud/api/consensus.js +259 -0
- package/dist/cloud/api/daemons.js +205 -1
- package/dist/cloud/api/git.js +37 -7
- package/dist/cloud/api/onboarding.js +4 -1
- package/dist/cloud/api/provider-env.d.ts +5 -0
- package/dist/cloud/api/provider-env.js +27 -0
- package/dist/cloud/api/providers.js +2 -0
- package/dist/cloud/api/test-helpers.js +130 -0
- package/dist/cloud/api/workspaces.js +38 -3
- package/dist/cloud/db/bulk-ingest.d.ts +88 -0
- package/dist/cloud/db/bulk-ingest.js +268 -0
- package/dist/cloud/db/drizzle.d.ts +33 -0
- package/dist/cloud/db/drizzle.js +174 -2
- package/dist/cloud/db/index.d.ts +24 -5
- package/dist/cloud/db/index.js +19 -4
- package/dist/cloud/db/schema.d.ts +397 -3
- package/dist/cloud/db/schema.js +75 -1
- package/dist/cloud/provisioner/index.d.ts +8 -0
- package/dist/cloud/provisioner/index.js +256 -50
- package/dist/cloud/server.js +47 -3
- package/dist/cloud/services/index.d.ts +1 -0
- package/dist/cloud/services/index.js +2 -0
- package/dist/cloud/services/nango.d.ts +3 -4
- package/dist/cloud/services/nango.js +11 -33
- package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
- package/dist/cloud/services/workspace-keepalive.js +234 -0
- package/dist/config/relay-config.d.ts +23 -0
- package/dist/config/relay-config.js +23 -0
- package/dist/daemon/agent-manager.d.ts +20 -1
- package/dist/daemon/agent-manager.js +51 -0
- package/dist/daemon/agent-registry.js +4 -4
- package/dist/daemon/agent-signing.d.ts +158 -0
- package/dist/daemon/agent-signing.js +523 -0
- package/dist/daemon/api.js +18 -1
- package/dist/daemon/cli-auth.d.ts +4 -1
- package/dist/daemon/cli-auth.js +55 -11
- package/dist/daemon/cloud-sync.d.ts +47 -1
- package/dist/daemon/cloud-sync.js +152 -3
- package/dist/daemon/connection.d.ts +28 -0
- package/dist/daemon/connection.js +113 -22
- package/dist/daemon/consensus-integration.d.ts +167 -0
- package/dist/daemon/consensus-integration.js +371 -0
- package/dist/daemon/consensus.d.ts +271 -0
- package/dist/daemon/consensus.js +632 -0
- package/dist/daemon/delivery-tracker.d.ts +34 -0
- package/dist/daemon/delivery-tracker.js +104 -0
- package/dist/daemon/enhanced-features.d.ts +118 -0
- package/dist/daemon/enhanced-features.js +178 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.js +5 -0
- package/dist/daemon/rate-limiter.d.ts +68 -0
- package/dist/daemon/rate-limiter.js +130 -0
- package/dist/daemon/router.d.ts +18 -11
- package/dist/daemon/router.js +57 -113
- package/dist/daemon/server.d.ts +13 -1
- package/dist/daemon/server.js +71 -9
- package/dist/daemon/sync-queue.d.ts +116 -0
- package/dist/daemon/sync-queue.js +361 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/{page-c617745b81344f4f.js → page-7f64824ae7d06707.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-dc786c183425c2ac.js → page-814efc4d77b4191d.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-2ee6beb2ae96d210.js → main-5a40a5ae29646e1b.js} +1 -1
- package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -0
- package/dist/dashboard/out/cloud/link.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +2 -2
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -3
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +1 -1
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +1 -1
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +1 -1
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +1 -1
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +1 -1
- package/dist/dashboard-server/server.js +244 -28
- package/dist/health-worker-manager.d.ts +62 -0
- package/dist/health-worker-manager.js +144 -0
- package/dist/health-worker.d.ts +9 -0
- package/dist/health-worker.js +79 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/memory/context-compaction.d.ts +156 -0
- package/dist/memory/context-compaction.js +453 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/protocol/channels.js +4 -4
- package/dist/protocol/framing.d.ts +72 -10
- package/dist/protocol/framing.js +194 -25
- package/dist/storage/adapter.d.ts +8 -1
- package/dist/storage/adapter.js +11 -0
- package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
- package/dist/storage/batched-sqlite-adapter.js +183 -0
- package/dist/storage/dead-letter-queue.d.ts +196 -0
- package/dist/storage/dead-letter-queue.js +427 -0
- package/dist/storage/dlq-adapter.d.ts +195 -0
- package/dist/storage/dlq-adapter.js +664 -0
- package/dist/trajectory/config.d.ts +32 -14
- package/dist/trajectory/config.js +38 -16
- package/dist/trajectory/integration.js +217 -64
- package/dist/utils/git-remote.d.ts +47 -0
- package/dist/utils/git-remote.js +125 -0
- package/dist/utils/id-generator.d.ts +35 -0
- package/dist/utils/id-generator.js +60 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/precompiled-patterns.d.ts +110 -0
- package/dist/utils/precompiled-patterns.js +322 -0
- package/dist/wrapper/auth-detection.js +1 -1
- package/dist/wrapper/base-wrapper.d.ts +40 -0
- package/dist/wrapper/base-wrapper.js +60 -6
- package/dist/wrapper/client.d.ts +14 -4
- package/dist/wrapper/client.js +89 -31
- package/dist/wrapper/idle-detector.d.ts +102 -0
- package/dist/wrapper/idle-detector.js +279 -0
- package/dist/wrapper/parser.d.ts +4 -0
- package/dist/wrapper/parser.js +19 -1
- package/dist/wrapper/pty-wrapper.d.ts +14 -2
- package/dist/wrapper/pty-wrapper.js +132 -32
- package/dist/wrapper/shared.d.ts +1 -1
- package/dist/wrapper/shared.js +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +20 -2
- package/dist/wrapper/tmux-wrapper.js +163 -40
- package/package.json +3 -1
- package/scripts/run-migrations.js +43 -0
- package/scripts/verify-schema.js +134 -0
- package/tests/benchmarks/protocol.bench.ts +310 -0
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +0 -1
- package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +0 -1
- /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
import { Router } from 'express';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
import { requireAuth } from './auth.js';
|
|
15
|
+
/**
|
|
16
|
+
* Hash an API key for lookup
|
|
17
|
+
*/
|
|
18
|
+
function hashApiKey(apiKey) {
|
|
19
|
+
return createHash('sha256').update(apiKey).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
export const consensusRouter = Router();
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// In-Memory Consensus State (synced from daemon)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Stores proposals synced from the daemon
|
|
26
|
+
// In production, this would be backed by a database
|
|
27
|
+
const workspaceProposals = new Map();
|
|
28
|
+
function getProposalsForWorkspace(workspaceId) {
|
|
29
|
+
let proposals = workspaceProposals.get(workspaceId);
|
|
30
|
+
if (!proposals) {
|
|
31
|
+
proposals = new Map();
|
|
32
|
+
workspaceProposals.set(workspaceId, proposals);
|
|
33
|
+
}
|
|
34
|
+
return proposals;
|
|
35
|
+
}
|
|
36
|
+
function computeStats(proposals) {
|
|
37
|
+
let pending = 0;
|
|
38
|
+
let approved = 0;
|
|
39
|
+
let rejected = 0;
|
|
40
|
+
let expired = 0;
|
|
41
|
+
let cancelled = 0;
|
|
42
|
+
for (const proposal of proposals.values()) {
|
|
43
|
+
switch (proposal.status) {
|
|
44
|
+
case 'pending':
|
|
45
|
+
pending++;
|
|
46
|
+
break;
|
|
47
|
+
case 'approved':
|
|
48
|
+
approved++;
|
|
49
|
+
break;
|
|
50
|
+
case 'rejected':
|
|
51
|
+
rejected++;
|
|
52
|
+
break;
|
|
53
|
+
case 'expired':
|
|
54
|
+
expired++;
|
|
55
|
+
break;
|
|
56
|
+
case 'cancelled':
|
|
57
|
+
cancelled++;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
total: proposals.size,
|
|
63
|
+
pending,
|
|
64
|
+
approved,
|
|
65
|
+
rejected,
|
|
66
|
+
expired,
|
|
67
|
+
cancelled,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Read-Only Routes (require user authentication)
|
|
72
|
+
// ============================================================================
|
|
73
|
+
/**
|
|
74
|
+
* GET /api/workspaces/:workspaceId/consensus/proposals
|
|
75
|
+
* List all proposals for a workspace (read-only)
|
|
76
|
+
*/
|
|
77
|
+
consensusRouter.get('/workspaces/:workspaceId/consensus/proposals', requireAuth, async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const { workspaceId } = req.params;
|
|
80
|
+
const { status, agent } = req.query;
|
|
81
|
+
const proposalsMap = getProposalsForWorkspace(workspaceId);
|
|
82
|
+
let proposals = Array.from(proposalsMap.values());
|
|
83
|
+
// Filter by agent if provided
|
|
84
|
+
if (agent && typeof agent === 'string') {
|
|
85
|
+
proposals = proposals.filter(p => p.proposer === agent || p.participants.includes(agent));
|
|
86
|
+
}
|
|
87
|
+
// Filter by status if provided
|
|
88
|
+
if (status && typeof status === 'string') {
|
|
89
|
+
proposals = proposals.filter(p => p.status === status);
|
|
90
|
+
}
|
|
91
|
+
// Sort by creation time (most recent first)
|
|
92
|
+
proposals.sort((a, b) => b.createdAt - a.createdAt);
|
|
93
|
+
const stats = computeStats(proposalsMap);
|
|
94
|
+
res.json({
|
|
95
|
+
proposals,
|
|
96
|
+
stats,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error('Error listing proposals:', error);
|
|
101
|
+
res.status(500).json({ error: 'Failed to list proposals' });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* GET /api/workspaces/:workspaceId/consensus/proposals/:proposalId
|
|
106
|
+
* Get a specific proposal (read-only)
|
|
107
|
+
*/
|
|
108
|
+
consensusRouter.get('/workspaces/:workspaceId/consensus/proposals/:proposalId', requireAuth, async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const { workspaceId, proposalId } = req.params;
|
|
111
|
+
const proposalsMap = getProposalsForWorkspace(workspaceId);
|
|
112
|
+
const proposal = proposalsMap.get(proposalId);
|
|
113
|
+
if (!proposal) {
|
|
114
|
+
return res.status(404).json({ error: 'Proposal not found' });
|
|
115
|
+
}
|
|
116
|
+
res.json({ proposal });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error('Error getting proposal:', error);
|
|
120
|
+
res.status(500).json({ error: 'Failed to get proposal' });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* GET /api/workspaces/:workspaceId/consensus/agents/:agentName/pending
|
|
125
|
+
* Get pending votes for an agent (read-only)
|
|
126
|
+
*/
|
|
127
|
+
consensusRouter.get('/workspaces/:workspaceId/consensus/agents/:agentName/pending', requireAuth, async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const { workspaceId, agentName } = req.params;
|
|
130
|
+
const proposalsMap = getProposalsForWorkspace(workspaceId);
|
|
131
|
+
const proposals = Array.from(proposalsMap.values()).filter(p => {
|
|
132
|
+
if (p.status !== 'pending')
|
|
133
|
+
return false;
|
|
134
|
+
if (!p.participants.includes(agentName))
|
|
135
|
+
return false;
|
|
136
|
+
// Check if agent hasn't voted yet
|
|
137
|
+
return !p.votes.some(v => v.agent === agentName);
|
|
138
|
+
});
|
|
139
|
+
res.json({ proposals });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error('Error getting pending votes:', error);
|
|
143
|
+
res.status(500).json({ error: 'Failed to get pending votes' });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* GET /api/workspaces/:workspaceId/consensus/stats
|
|
148
|
+
* Get consensus statistics for a workspace (read-only)
|
|
149
|
+
*/
|
|
150
|
+
consensusRouter.get('/workspaces/:workspaceId/consensus/stats', requireAuth, async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const { workspaceId } = req.params;
|
|
153
|
+
const proposalsMap = getProposalsForWorkspace(workspaceId);
|
|
154
|
+
const stats = computeStats(proposalsMap);
|
|
155
|
+
res.json({ stats });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error('Error getting consensus stats:', error);
|
|
159
|
+
res.status(500).json({ error: 'Failed to get consensus stats' });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Sync Endpoint (daemon -> cloud)
|
|
164
|
+
// ============================================================================
|
|
165
|
+
/**
|
|
166
|
+
* POST /api/daemons/consensus/sync
|
|
167
|
+
* Sync consensus state from daemon (called by daemon on proposal events)
|
|
168
|
+
*
|
|
169
|
+
* This endpoint receives state updates from the daemon and stores them
|
|
170
|
+
* so the dashboard can display agent consensus activity.
|
|
171
|
+
*
|
|
172
|
+
* Authentication options (in order of precedence):
|
|
173
|
+
* 1. Daemon API key (Authorization: Bearer ar_live_xxx) - workspace from daemon record
|
|
174
|
+
* 2. Workspace ID in request body - for self-hosted setups
|
|
175
|
+
* 3. Default workspace "local" - for simple local development
|
|
176
|
+
*/
|
|
177
|
+
consensusRouter.post('/daemons/consensus/sync', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const { proposal, event, workspaceId: bodyWorkspaceId } = req.body;
|
|
180
|
+
if (!proposal || !event) {
|
|
181
|
+
return res.status(400).json({ error: 'Missing proposal or event' });
|
|
182
|
+
}
|
|
183
|
+
let workspaceId;
|
|
184
|
+
// Try to authenticate via API key first
|
|
185
|
+
const authHeader = req.headers.authorization;
|
|
186
|
+
if (authHeader?.startsWith('Bearer ar_live_')) {
|
|
187
|
+
const apiKey = authHeader.replace('Bearer ', '');
|
|
188
|
+
const apiKeyHash = hashApiKey(apiKey);
|
|
189
|
+
const { db } = await import('../db/index.js');
|
|
190
|
+
const daemon = await db.linkedDaemons.findByApiKeyHash(apiKeyHash);
|
|
191
|
+
if (daemon?.workspaceId) {
|
|
192
|
+
workspaceId = daemon.workspaceId;
|
|
193
|
+
}
|
|
194
|
+
else if (bodyWorkspaceId) {
|
|
195
|
+
workspaceId = bodyWorkspaceId;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
return res.status(400).json({ error: 'Daemon not associated with a workspace' });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (bodyWorkspaceId) {
|
|
202
|
+
// Self-hosted: workspace specified in body
|
|
203
|
+
workspaceId = bodyWorkspaceId;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Default for simple local setups
|
|
207
|
+
workspaceId = 'local';
|
|
208
|
+
}
|
|
209
|
+
// Store/update the proposal
|
|
210
|
+
const proposalsMap = getProposalsForWorkspace(workspaceId);
|
|
211
|
+
proposalsMap.set(proposal.id, proposal);
|
|
212
|
+
console.log(`[consensus] Synced ${event} for proposal "${proposal.title}" (${proposal.id}) in workspace ${workspaceId}`);
|
|
213
|
+
res.json({ success: true, workspaceId });
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error('Error syncing consensus:', error);
|
|
217
|
+
res.status(500).json({ error: 'Failed to sync consensus' });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* DELETE /api/daemons/consensus/proposals/:proposalId
|
|
222
|
+
* Remove a proposal from the sync cache (daemon cleanup)
|
|
223
|
+
*
|
|
224
|
+
* Authentication: Uses daemon API key (Authorization: Bearer ar_live_xxx)
|
|
225
|
+
* Workspace is derived from the linked daemon's record.
|
|
226
|
+
*/
|
|
227
|
+
consensusRouter.delete('/daemons/consensus/proposals/:proposalId', async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
// Check for daemon API key (Bearer token)
|
|
230
|
+
const authHeader = req.headers.authorization;
|
|
231
|
+
if (!authHeader?.startsWith('Bearer ar_live_')) {
|
|
232
|
+
return res.status(401).json({ error: 'Unauthorized - daemon API key required' });
|
|
233
|
+
}
|
|
234
|
+
// Validate the API key
|
|
235
|
+
const apiKey = authHeader.replace('Bearer ', '');
|
|
236
|
+
const apiKeyHash = hashApiKey(apiKey);
|
|
237
|
+
const { db } = await import('../db/index.js');
|
|
238
|
+
const daemon = await db.linkedDaemons.findByApiKeyHash(apiKeyHash);
|
|
239
|
+
if (!daemon) {
|
|
240
|
+
return res.status(401).json({ error: 'Invalid API key' });
|
|
241
|
+
}
|
|
242
|
+
if (!daemon.workspaceId) {
|
|
243
|
+
return res.status(400).json({ error: 'Daemon not associated with a workspace' });
|
|
244
|
+
}
|
|
245
|
+
const { proposalId } = req.params;
|
|
246
|
+
const proposalsMap = getProposalsForWorkspace(daemon.workspaceId);
|
|
247
|
+
const deleted = proposalsMap.delete(proposalId);
|
|
248
|
+
if (!deleted) {
|
|
249
|
+
return res.status(404).json({ error: 'Proposal not found' });
|
|
250
|
+
}
|
|
251
|
+
console.log(`[consensus] Removed proposal ${proposalId} from workspace ${daemon.workspaceId}`);
|
|
252
|
+
res.json({ success: true });
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.error('Error removing proposal:', error);
|
|
256
|
+
res.status(500).json({ error: 'Failed to remove proposal' });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
//# sourceMappingURL=consensus.js.map
|
|
@@ -40,11 +40,25 @@ function hashApiKey(apiKey) {
|
|
|
40
40
|
*/
|
|
41
41
|
daemonsRouter.post('/link', requireAuth, async (req, res) => {
|
|
42
42
|
const userId = req.session.userId;
|
|
43
|
-
const { name, machineId, metadata } = req.body;
|
|
43
|
+
const { name, machineId, metadata, workspaceId } = req.body;
|
|
44
44
|
if (!machineId || typeof machineId !== 'string') {
|
|
45
45
|
return res.status(400).json({ error: 'machineId is required' });
|
|
46
46
|
}
|
|
47
47
|
try {
|
|
48
|
+
// Validate workspace ownership if provided
|
|
49
|
+
if (workspaceId) {
|
|
50
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
51
|
+
if (!workspace) {
|
|
52
|
+
return res.status(404).json({ error: 'Workspace not found' });
|
|
53
|
+
}
|
|
54
|
+
if (workspace.userId !== userId) {
|
|
55
|
+
// Check if user is a member of the workspace
|
|
56
|
+
const member = await db.workspaceMembers.findMembership(workspaceId, userId);
|
|
57
|
+
if (!member) {
|
|
58
|
+
return res.status(403).json({ error: 'Not authorized to link to this workspace' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
48
62
|
// Check if this machine is already linked
|
|
49
63
|
const existing = await db.linkedDaemons.findByMachineId(userId, machineId);
|
|
50
64
|
if (existing) {
|
|
@@ -54,6 +68,7 @@ daemonsRouter.post('/link', requireAuth, async (req, res) => {
|
|
|
54
68
|
await db.linkedDaemons.update(existing.id, {
|
|
55
69
|
name: name || existing.name,
|
|
56
70
|
apiKeyHash,
|
|
71
|
+
workspaceId: workspaceId || existing.workspaceId,
|
|
57
72
|
metadata: metadata || existing.metadata,
|
|
58
73
|
status: 'online',
|
|
59
74
|
lastSeenAt: new Date(),
|
|
@@ -61,6 +76,7 @@ daemonsRouter.post('/link', requireAuth, async (req, res) => {
|
|
|
61
76
|
return res.json({
|
|
62
77
|
success: true,
|
|
63
78
|
daemonId: existing.id,
|
|
79
|
+
workspaceId: workspaceId || existing.workspaceId,
|
|
64
80
|
apiKey, // Only returned once!
|
|
65
81
|
message: 'Daemon re-linked with new API key',
|
|
66
82
|
});
|
|
@@ -70,6 +86,7 @@ daemonsRouter.post('/link', requireAuth, async (req, res) => {
|
|
|
70
86
|
const apiKeyHash = hashApiKey(apiKey);
|
|
71
87
|
const daemon = await db.linkedDaemons.create({
|
|
72
88
|
userId,
|
|
89
|
+
workspaceId: workspaceId || null,
|
|
73
90
|
name: name || `Daemon on ${machineId.substring(0, 8)}`,
|
|
74
91
|
machineId,
|
|
75
92
|
apiKeyHash,
|
|
@@ -79,6 +96,7 @@ daemonsRouter.post('/link', requireAuth, async (req, res) => {
|
|
|
79
96
|
res.status(201).json({
|
|
80
97
|
success: true,
|
|
81
98
|
daemonId: daemon.id,
|
|
99
|
+
workspaceId: workspaceId || null,
|
|
82
100
|
apiKey, // Only returned once - user must save this!
|
|
83
101
|
message: 'Daemon linked successfully. Save your API key - it cannot be retrieved later.',
|
|
84
102
|
});
|
|
@@ -113,6 +131,59 @@ daemonsRouter.get('/', requireAuth, async (req, res) => {
|
|
|
113
131
|
res.status(500).json({ error: 'Failed to list daemons' });
|
|
114
132
|
}
|
|
115
133
|
});
|
|
134
|
+
/**
|
|
135
|
+
* GET /api/daemons/workspace/:workspaceId/agents
|
|
136
|
+
* Get local agents for a specific workspace
|
|
137
|
+
*/
|
|
138
|
+
daemonsRouter.get('/workspace/:workspaceId/agents', requireAuth, async (req, res) => {
|
|
139
|
+
const userId = req.session.userId;
|
|
140
|
+
const { workspaceId } = req.params;
|
|
141
|
+
try {
|
|
142
|
+
// Verify user has access to this workspace
|
|
143
|
+
const workspace = await db.workspaces.findById(workspaceId);
|
|
144
|
+
if (!workspace) {
|
|
145
|
+
return res.status(404).json({ error: 'Workspace not found' });
|
|
146
|
+
}
|
|
147
|
+
// Check if user owns the workspace or is a member
|
|
148
|
+
if (workspace.userId !== userId) {
|
|
149
|
+
const member = await db.workspaceMembers.findMembership(workspaceId, userId);
|
|
150
|
+
if (!member) {
|
|
151
|
+
return res.status(403).json({ error: 'Not authorized to access this workspace' });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Get all linked daemons for this workspace
|
|
155
|
+
const daemons = await db.linkedDaemons.findByWorkspaceId(workspaceId);
|
|
156
|
+
// Extract agents from each daemon's metadata
|
|
157
|
+
const localAgents = daemons.flatMap((daemon) => {
|
|
158
|
+
const metadata = daemon.metadata;
|
|
159
|
+
const agents = metadata?.agents || [];
|
|
160
|
+
return agents.map((agent) => ({
|
|
161
|
+
name: agent.name,
|
|
162
|
+
status: agent.status,
|
|
163
|
+
isLocal: true,
|
|
164
|
+
daemonId: daemon.id,
|
|
165
|
+
daemonName: daemon.name,
|
|
166
|
+
daemonStatus: daemon.status,
|
|
167
|
+
machineId: daemon.machineId,
|
|
168
|
+
lastSeenAt: daemon.lastSeenAt,
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
res.json({
|
|
172
|
+
agents: localAgents,
|
|
173
|
+
daemons: daemons.map((d) => ({
|
|
174
|
+
id: d.id,
|
|
175
|
+
name: d.name,
|
|
176
|
+
machineId: d.machineId,
|
|
177
|
+
status: d.status,
|
|
178
|
+
lastSeenAt: d.lastSeenAt,
|
|
179
|
+
})),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error('Error fetching local agents:', error);
|
|
184
|
+
res.status(500).json({ error: 'Failed to fetch local agents' });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
116
187
|
/**
|
|
117
188
|
* DELETE /api/daemons/:id
|
|
118
189
|
* Unlink a daemon
|
|
@@ -321,4 +392,137 @@ daemonsRouter.get('/messages', requireDaemonAuth, async (req, res) => {
|
|
|
321
392
|
res.status(500).json({ error: 'Failed to fetch messages' });
|
|
322
393
|
}
|
|
323
394
|
});
|
|
395
|
+
/**
|
|
396
|
+
* POST /api/daemons/messages/sync
|
|
397
|
+
* Sync messages from daemon to cloud storage
|
|
398
|
+
*
|
|
399
|
+
* Accepts batches of messages and stores them in agent_messages table.
|
|
400
|
+
* Uses upsert logic to handle duplicates (based on workspace_id + original_id).
|
|
401
|
+
*
|
|
402
|
+
* Request body:
|
|
403
|
+
* {
|
|
404
|
+
* messages: SyncMessageInput[]
|
|
405
|
+
* }
|
|
406
|
+
*
|
|
407
|
+
* Response:
|
|
408
|
+
* {
|
|
409
|
+
* success: true,
|
|
410
|
+
* synced: number, // Count of messages synced
|
|
411
|
+
* duplicates: number // Count of messages skipped (already existed)
|
|
412
|
+
* }
|
|
413
|
+
*/
|
|
414
|
+
daemonsRouter.post('/messages/sync', requireDaemonAuth, async (req, res) => {
|
|
415
|
+
const daemon = req.daemon;
|
|
416
|
+
const { messages, repoFullName } = req.body;
|
|
417
|
+
if (!messages || !Array.isArray(messages)) {
|
|
418
|
+
return res.status(400).json({ error: 'messages array is required' });
|
|
419
|
+
}
|
|
420
|
+
if (messages.length === 0) {
|
|
421
|
+
return res.json({ success: true, synced: 0, duplicates: 0 });
|
|
422
|
+
}
|
|
423
|
+
// Limit batch size to prevent abuse
|
|
424
|
+
if (messages.length > 500) {
|
|
425
|
+
return res.status(400).json({ error: 'Maximum batch size is 500 messages' });
|
|
426
|
+
}
|
|
427
|
+
// Resolve workspace from git remote if not already linked
|
|
428
|
+
let workspaceId = daemon.workspaceId;
|
|
429
|
+
if (!workspaceId && repoFullName) {
|
|
430
|
+
// Try to find workspace by repository
|
|
431
|
+
const workspace = await db.workspaces.findByRepoFullName(repoFullName);
|
|
432
|
+
if (workspace) {
|
|
433
|
+
// Auto-link daemon to workspace
|
|
434
|
+
await db.linkedDaemons.update(daemon.id, { workspaceId: workspace.id });
|
|
435
|
+
workspaceId = workspace.id;
|
|
436
|
+
console.log(`[message-sync] Auto-linked daemon ${daemon.id} to workspace ${workspace.id} via repo ${repoFullName}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Require workspace to be linked
|
|
440
|
+
if (!workspaceId) {
|
|
441
|
+
const hint = repoFullName
|
|
442
|
+
? `Repository '${repoFullName}' not found in any workspace. Link the repo in the dashboard first.`
|
|
443
|
+
: 'Daemon must be linked to a workspace to sync messages. Re-link with a workspace ID.';
|
|
444
|
+
return res.status(400).json({ error: hint });
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
// Get user plan to determine retention policy
|
|
448
|
+
const user = await db.users.findById(daemon.userId);
|
|
449
|
+
const plan = user?.plan || 'free';
|
|
450
|
+
// Calculate expires_at based on plan
|
|
451
|
+
let expiresAt = null;
|
|
452
|
+
if (plan === 'free') {
|
|
453
|
+
expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
|
454
|
+
}
|
|
455
|
+
else if (plan === 'pro') {
|
|
456
|
+
expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days
|
|
457
|
+
}
|
|
458
|
+
// Enterprise: null (never expires)
|
|
459
|
+
// Transform to NewAgentMessage format
|
|
460
|
+
const dbMessages = messages.map((msg) => ({
|
|
461
|
+
workspaceId,
|
|
462
|
+
daemonId: daemon.id,
|
|
463
|
+
originalId: msg.id,
|
|
464
|
+
fromAgent: msg.from,
|
|
465
|
+
toAgent: msg.to,
|
|
466
|
+
body: msg.body,
|
|
467
|
+
kind: msg.kind || 'message',
|
|
468
|
+
topic: msg.topic || null,
|
|
469
|
+
thread: msg.thread || null,
|
|
470
|
+
channel: msg.channel || null,
|
|
471
|
+
isBroadcast: msg.is_broadcast || msg.to === '*',
|
|
472
|
+
isUrgent: msg.is_urgent || false,
|
|
473
|
+
data: msg.data || null,
|
|
474
|
+
payloadMeta: msg.payload_meta || null,
|
|
475
|
+
messageTs: new Date(msg.ts),
|
|
476
|
+
expiresAt,
|
|
477
|
+
}));
|
|
478
|
+
// Use optimized bulk insert for high-volume message sync
|
|
479
|
+
// - Batches < 100: multi-row INSERT
|
|
480
|
+
// - Batches 100-1000: chunked multi-row INSERT
|
|
481
|
+
// - Batches > 1000: streaming COPY with staging table
|
|
482
|
+
const result = await db.bulk.optimizedInsert(db.getRawPool(), dbMessages);
|
|
483
|
+
const synced = result.inserted;
|
|
484
|
+
const duplicates = result.duplicates;
|
|
485
|
+
console.log(`[message-sync] Synced ${synced} messages for daemon ${daemon.id}, ${duplicates} duplicates skipped (${result.durationMs}ms)`);
|
|
486
|
+
res.json({
|
|
487
|
+
success: true,
|
|
488
|
+
synced,
|
|
489
|
+
duplicates,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.error('Error syncing messages:', error);
|
|
494
|
+
res.status(500).json({ error: 'Failed to sync messages' });
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
/**
|
|
498
|
+
* GET /api/daemons/messages/stats
|
|
499
|
+
* Get message sync statistics for this daemon's workspace
|
|
500
|
+
*/
|
|
501
|
+
daemonsRouter.get('/messages/stats', requireDaemonAuth, async (req, res) => {
|
|
502
|
+
const daemon = req.daemon;
|
|
503
|
+
if (!daemon.workspaceId) {
|
|
504
|
+
return res.status(400).json({ error: 'Daemon must be linked to a workspace' });
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
// Get message count and pool health in parallel
|
|
508
|
+
const [count, poolHealth, poolStats] = await Promise.all([
|
|
509
|
+
db.agentMessages.countByWorkspace(daemon.workspaceId),
|
|
510
|
+
db.bulk.checkHealth(),
|
|
511
|
+
Promise.resolve(db.bulk.getPoolStats()),
|
|
512
|
+
]);
|
|
513
|
+
res.json({
|
|
514
|
+
workspaceId: daemon.workspaceId,
|
|
515
|
+
messageCount: count,
|
|
516
|
+
database: {
|
|
517
|
+
healthy: poolHealth.healthy,
|
|
518
|
+
latencyMs: poolHealth.latencyMs,
|
|
519
|
+
pool: poolStats,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
console.error('Error fetching message stats:', error);
|
|
525
|
+
res.status(500).json({ error: 'Failed to fetch message stats' });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
324
528
|
//# sourceMappingURL=daemons.js.map
|
package/dist/cloud/api/git.js
CHANGED
|
@@ -152,12 +152,19 @@ gitRouter.get('/token', async (req, res) => {
|
|
|
152
152
|
}
|
|
153
153
|
// GitHub App installation tokens expire after 1 hour
|
|
154
154
|
const expiresAt = new Date(Date.now() + 55 * 60 * 1000).toISOString(); // 55 min buffer
|
|
155
|
-
|
|
155
|
+
// Prefer userToken for git operations - installation tokens (ghs_*) are API-only
|
|
156
|
+
// and don't work with git credential helpers. User OAuth tokens work for both
|
|
157
|
+
// git operations (clone, push, pull) AND gh CLI commands.
|
|
158
|
+
const primaryToken = userToken || installationToken;
|
|
159
|
+
const tokenType = userToken ? 'user' : 'installation';
|
|
160
|
+
console.log(`[git] Token fetched successfully for workspace ${workspaceId.substring(0, 8)} (type: ${tokenType})`);
|
|
156
161
|
res.json({
|
|
157
|
-
token:
|
|
158
|
-
userToken, //
|
|
162
|
+
token: primaryToken, // Primary token for git/gh operations (prefer user token)
|
|
163
|
+
userToken, // Explicit user token field (may be null)
|
|
164
|
+
installationToken, // GitHub App installation token for API operations
|
|
159
165
|
expiresAt,
|
|
160
|
-
username: 'x-access-token', //
|
|
166
|
+
username: 'x-access-token', // Works with both token types
|
|
167
|
+
tokenType, // 'user' or 'installation' - helps clients know what they got
|
|
161
168
|
});
|
|
162
169
|
}
|
|
163
170
|
catch (error) {
|
|
@@ -206,9 +213,9 @@ gitRouter.post('/token', async (req, res) => {
|
|
|
206
213
|
code: 'NO_GITHUB_APP_CONNECTION',
|
|
207
214
|
});
|
|
208
215
|
}
|
|
209
|
-
let
|
|
216
|
+
let installationToken;
|
|
210
217
|
try {
|
|
211
|
-
|
|
218
|
+
installationToken = await nangoService.getGithubAppToken(repoWithConnection.nangoConnectionId);
|
|
212
219
|
}
|
|
213
220
|
catch (nangoError) {
|
|
214
221
|
const errorMessage = nangoError instanceof Error ? nangoError.message : 'Unknown error';
|
|
@@ -219,11 +226,34 @@ gitRouter.post('/token', async (req, res) => {
|
|
|
219
226
|
details: errorMessage,
|
|
220
227
|
});
|
|
221
228
|
}
|
|
229
|
+
// Try to get user OAuth token (preferred for git operations)
|
|
230
|
+
let userToken = null;
|
|
231
|
+
try {
|
|
232
|
+
userToken = await nangoService.getGithubUserOAuthToken(repoWithConnection.nangoConnectionId);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Try the separate github user connection if available
|
|
236
|
+
const userRepo = repos.find(r => r.nangoConnectionId && r.nangoConnectionId !== repoWithConnection.nangoConnectionId);
|
|
237
|
+
if (userRepo?.nangoConnectionId) {
|
|
238
|
+
try {
|
|
239
|
+
userToken = await nangoService.getGithubUserToken(userRepo.nangoConnectionId);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
console.log('[git] POST: No github user token available');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
222
246
|
const expiresAt = new Date(Date.now() + 55 * 60 * 1000).toISOString();
|
|
247
|
+
// Prefer userToken for git operations
|
|
248
|
+
const primaryToken = userToken || installationToken;
|
|
249
|
+
const tokenType = userToken ? 'user' : 'installation';
|
|
223
250
|
res.json({
|
|
224
|
-
token,
|
|
251
|
+
token: primaryToken,
|
|
252
|
+
userToken,
|
|
253
|
+
installationToken,
|
|
225
254
|
expiresAt,
|
|
226
255
|
username: 'x-access-token',
|
|
256
|
+
tokenType,
|
|
227
257
|
});
|
|
228
258
|
}
|
|
229
259
|
catch (error) {
|
|
@@ -13,6 +13,7 @@ import { Router } from 'express';
|
|
|
13
13
|
import * as crypto from 'crypto';
|
|
14
14
|
import { requireAuth } from './auth.js';
|
|
15
15
|
import { db } from '../db/index.js';
|
|
16
|
+
import { setProviderApiKeyEnv } from './provider-env.js';
|
|
16
17
|
// Import for local use
|
|
17
18
|
import { CLI_AUTH_CONFIG, runCLIAuthViaPTY, stripAnsiCodes, matchesSuccessPattern, findMatchingPrompt, validateProviderConfig, validateAllProviderConfigs, getSupportedProviders, } from './cli-pty-runner.js';
|
|
18
19
|
// Re-export from shared module for backward compatibility
|
|
@@ -121,10 +122,11 @@ onboardingRouter.post('/cli/:provider/start', async (req, res) => {
|
|
|
121
122
|
}
|
|
122
123
|
const targetUrl = `${workspaceUrl}/auth/cli/${provider}/start`;
|
|
123
124
|
console.log('[onboarding] Forwarding to workspace daemon:', targetUrl);
|
|
125
|
+
// Pass userId to enable per-user credential storage in multi-user workspaces
|
|
124
126
|
const authResponse = await fetch(targetUrl, {
|
|
125
127
|
method: 'POST',
|
|
126
128
|
headers: { 'Content-Type': 'application/json' },
|
|
127
|
-
body: JSON.stringify({ useDeviceFlow }),
|
|
129
|
+
body: JSON.stringify({ useDeviceFlow, userId }),
|
|
128
130
|
});
|
|
129
131
|
console.log('[onboarding] Workspace daemon response:', authResponse.status);
|
|
130
132
|
if (!authResponse.ok) {
|
|
@@ -469,6 +471,7 @@ onboardingRouter.post('/token/:provider', async (req, res) => {
|
|
|
469
471
|
scopes: getProviderScopes(provider),
|
|
470
472
|
providerAccountEmail: email,
|
|
471
473
|
});
|
|
474
|
+
await setProviderApiKeyEnv(userId, provider, token);
|
|
472
475
|
res.json({
|
|
473
476
|
success: true,
|
|
474
477
|
message: `${provider} connected successfully`,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { db } from '../db/index.js';
|
|
2
|
+
import { getProvisioner } from '../provisioner/index.js';
|
|
3
|
+
const PROVIDER_ENV_VARS = {
|
|
4
|
+
google: 'GEMINI_API_KEY',
|
|
5
|
+
gemini: 'GEMINI_API_KEY',
|
|
6
|
+
};
|
|
7
|
+
export async function setProviderApiKeyEnv(userId, provider, apiKey) {
|
|
8
|
+
const envVarName = PROVIDER_ENV_VARS[provider];
|
|
9
|
+
if (!envVarName) {
|
|
10
|
+
return { updated: 0, skipped: 0 };
|
|
11
|
+
}
|
|
12
|
+
const workspaces = await db.workspaces.findByUserId(userId);
|
|
13
|
+
if (workspaces.length === 0) {
|
|
14
|
+
return { updated: 0, skipped: 0 };
|
|
15
|
+
}
|
|
16
|
+
const provisioner = getProvisioner();
|
|
17
|
+
const results = await Promise.all(workspaces.map(async (workspace) => {
|
|
18
|
+
if (!workspace.computeId) {
|
|
19
|
+
return 'skipped';
|
|
20
|
+
}
|
|
21
|
+
await provisioner.setWorkspaceEnvVars(workspace, { [envVarName]: apiKey });
|
|
22
|
+
return 'updated';
|
|
23
|
+
}));
|
|
24
|
+
const updated = results.filter((result) => result === 'updated').length;
|
|
25
|
+
return { updated, skipped: results.length - updated };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=provider-env.js.map
|
|
@@ -9,6 +9,7 @@ import { createClient } from 'redis';
|
|
|
9
9
|
import { requireAuth } from './auth.js';
|
|
10
10
|
import { getConfig } from '../config.js';
|
|
11
11
|
import { db } from '../db/index.js';
|
|
12
|
+
import { setProviderApiKeyEnv } from './provider-env.js';
|
|
12
13
|
export const providersRouter = Router();
|
|
13
14
|
// All routes require authentication
|
|
14
15
|
providersRouter.use(requireAuth);
|
|
@@ -312,6 +313,7 @@ providersRouter.post('/:provider/api-key', async (req, res) => {
|
|
|
312
313
|
provider,
|
|
313
314
|
scopes,
|
|
314
315
|
});
|
|
316
|
+
await setProviderApiKeyEnv(userId, provider, apiKey);
|
|
315
317
|
res.json({
|
|
316
318
|
success: true,
|
|
317
319
|
message: `${providerConfig.displayName} connected`,
|