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.
- package/.trajectories/agent-relay-322-324.md +17 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.json +49 -0
- package/.trajectories/completed/2026-01/traj_03zupyv1s7b9.md +31 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.json +125 -0
- package/.trajectories/completed/2026-01/traj_0zacdjl1g4ht.md +62 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.json +49 -0
- package/.trajectories/completed/2026-01/traj_33iuy72sezbk.md +31 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.json +77 -0
- package/.trajectories/completed/2026-01/traj_5ammh5qtvklq.md +42 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.json +77 -0
- package/.trajectories/completed/2026-01/traj_6mieijqyvaag.md +42 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.json +77 -0
- package/.trajectories/completed/2026-01/traj_78ffm31jn3uk.md +42 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.json +66 -0
- package/.trajectories/completed/2026-01/traj_94gnp3k30goq.md +36 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.json +40 -0
- package/.trajectories/completed/2026-01/traj_avqeghu6pz5a.md +22 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.json +121 -0
- package/.trajectories/completed/2026-01/traj_dcsp9s8y01ra.md +29 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.json +53 -0
- package/.trajectories/completed/2026-01/traj_fhx9irlckht6.md +32 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.json +101 -0
- package/.trajectories/completed/2026-01/traj_fqduidx3xbtp.md +52 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.json +49 -0
- package/.trajectories/completed/2026-01/traj_hf81ey93uz6t.md +31 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.json +65 -0
- package/.trajectories/completed/2026-01/traj_hfmki2jr9d4r.md +37 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.json +49 -0
- package/.trajectories/completed/2026-01/traj_lq450ly148uw.md +31 -0
- package/.trajectories/completed/2026-01/traj_multi_server_arch.md +101 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.json +27 -0
- package/.trajectories/completed/2026-01/traj_psd9ob0j2ru3.md +14 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.json +53 -0
- package/.trajectories/completed/2026-01/traj_ub8csuv3lcv4.md +32 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.json +186 -0
- package/.trajectories/completed/2026-01/traj_uc29tlso8i9s.md +86 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json +77 -0
- package/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.md +42 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.json +89 -0
- package/.trajectories/completed/2026-01/traj_v9dkdoxylyid.md +47 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.json +65 -0
- package/.trajectories/completed/2026-01/traj_xy9vifpqet80.md +37 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7aiwijyfmmv.md +31 -0
- package/.trajectories/consolidate-settings-panel.md +24 -0
- package/.trajectories/gh-cli-user-token.md +26 -0
- package/.trajectories/index.json +155 -1
- package/deploy/workspace/codex.config.toml +15 -0
- package/deploy/workspace/entrypoint.sh +167 -7
- package/deploy/workspace/git-credential-relay +17 -2
- package/dist/bridge/spawner.d.ts +7 -0
- package/dist/bridge/spawner.js +40 -9
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.js +210 -168
- package/dist/cloud/api/admin.d.ts +8 -0
- package/dist/cloud/api/admin.js +212 -0
- package/dist/cloud/api/auth.js +8 -0
- package/dist/cloud/api/billing.d.ts +0 -10
- package/dist/cloud/api/billing.js +248 -58
- package/dist/cloud/api/codex-auth-helper.d.ts +10 -4
- package/dist/cloud/api/codex-auth-helper.js +215 -8
- package/dist/cloud/api/coordinators.js +402 -0
- package/dist/cloud/api/daemons.js +15 -11
- package/dist/cloud/api/git.js +104 -17
- package/dist/cloud/api/github-app.js +42 -8
- package/dist/cloud/api/nango-auth.js +297 -16
- package/dist/cloud/api/onboarding.js +97 -33
- package/dist/cloud/api/providers.js +12 -16
- package/dist/cloud/api/repos.js +200 -124
- package/dist/cloud/api/test-helpers.js +40 -0
- package/dist/cloud/api/usage.js +13 -0
- package/dist/cloud/api/webhooks.js +1 -1
- package/dist/cloud/api/workspaces.d.ts +18 -0
- package/dist/cloud/api/workspaces.js +945 -15
- package/dist/cloud/config.d.ts +8 -0
- package/dist/cloud/config.js +15 -0
- package/dist/cloud/db/drizzle.d.ts +5 -2
- package/dist/cloud/db/drizzle.js +27 -20
- package/dist/cloud/db/schema.d.ts +19 -51
- package/dist/cloud/db/schema.js +5 -4
- package/dist/cloud/index.d.ts +0 -1
- package/dist/cloud/index.js +0 -1
- package/dist/cloud/provisioner/index.d.ts +93 -1
- package/dist/cloud/provisioner/index.js +608 -63
- package/dist/cloud/server.js +156 -16
- package/dist/cloud/services/compute-enforcement.d.ts +57 -0
- package/dist/cloud/services/compute-enforcement.js +175 -0
- package/dist/cloud/services/index.d.ts +2 -0
- package/dist/cloud/services/index.js +4 -0
- package/dist/cloud/services/intro-expiration.d.ts +55 -0
- package/dist/cloud/services/intro-expiration.js +211 -0
- package/dist/cloud/services/nango.d.ts +14 -0
- package/dist/cloud/services/nango.js +74 -14
- package/dist/cloud/services/ssh-security.d.ts +31 -0
- package/dist/cloud/services/ssh-security.js +63 -0
- package/dist/continuity/manager.d.ts +5 -0
- package/dist/continuity/manager.js +56 -2
- package/dist/daemon/api.d.ts +2 -0
- package/dist/daemon/api.js +214 -5
- package/dist/daemon/cli-auth.d.ts +13 -1
- package/dist/daemon/cli-auth.js +166 -47
- package/dist/daemon/connection.d.ts +7 -1
- package/dist/daemon/connection.js +15 -0
- package/dist/daemon/orchestrator.d.ts +2 -0
- package/dist/daemon/orchestrator.js +26 -0
- package/dist/daemon/repo-manager.d.ts +116 -0
- package/dist/daemon/repo-manager.js +384 -0
- package/dist/daemon/router.d.ts +60 -1
- package/dist/daemon/router.js +281 -20
- package/dist/daemon/user-directory.d.ts +111 -0
- package/dist/daemon/user-directory.js +233 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/{page-3fdfa60e53f2810d.js → page-8553743baca53a00.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-c617745b81344f4f.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-77e9c65420a06cfb.js → page-dc786c183425c2ac.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-ed4e1fb6f29c34cf.js → main-2ee6beb2ae96d210.js} +1 -1
- package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +1 -0
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
- package/dist/dashboard/out/_next/static/sDcbGRTYLcpPvyTs_rsNb/_ssgManifest.js +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +3 -3
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +3 -3
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +2 -2
- 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 +3 -3
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +3 -3
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +3 -3
- package/dist/dashboard/out/providers/setup/claude.html +1 -0
- package/dist/dashboard/out/providers/setup/claude.txt +8 -0
- package/dist/dashboard/out/providers/setup/codex.html +1 -0
- package/dist/dashboard/out/providers/setup/codex.txt +8 -0
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +3 -3
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +2 -2
- package/dist/dashboard-server/server.js +316 -12
- package/dist/dashboard-server/user-bridge.d.ts +103 -0
- package/dist/dashboard-server/user-bridge.js +189 -0
- package/dist/protocol/channels.d.ts +205 -0
- package/dist/protocol/channels.js +154 -0
- package/dist/protocol/types.d.ts +13 -1
- package/dist/resiliency/provider-context.js +2 -0
- package/dist/shared/cli-auth-config.d.ts +19 -0
- package/dist/shared/cli-auth-config.js +58 -2
- package/dist/utils/agent-config.js +1 -1
- package/dist/wrapper/auth-detection.d.ts +49 -0
- package/dist/wrapper/auth-detection.js +192 -0
- package/dist/wrapper/base-wrapper.d.ts +153 -0
- package/dist/wrapper/base-wrapper.js +393 -0
- package/dist/wrapper/client.d.ts +7 -1
- package/dist/wrapper/client.js +3 -0
- package/dist/wrapper/index.d.ts +1 -0
- package/dist/wrapper/index.js +4 -3
- package/dist/wrapper/pty-wrapper.d.ts +62 -84
- package/dist/wrapper/pty-wrapper.js +154 -180
- package/dist/wrapper/tmux-wrapper.d.ts +41 -66
- package/dist/wrapper/tmux-wrapper.js +90 -134
- package/package.json +4 -2
- package/scripts/postinstall.js +11 -155
- package/scripts/test-interactive-terminal.sh +248 -0
- package/dist/cloud/vault/index.d.ts +0 -76
- package/dist/cloud/vault/index.js +0 -219
- package/dist/dashboard/out/_next/static/chunks/699-3b1cd6618a45d259.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/724-2dae7627550ab88f.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/766-1f2dd8cb7f766b0b.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e6381e5a6e1fbcfd.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-67a3e98d9a43a6ed.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-e88bc117ef7671c3.js +0 -1
- package/dist/dashboard/out/_next/static/css/29852f26181969a0.css +0 -1
- package/dist/dashboard/out/_next/static/css/7c3ae9e8617d42a5.css +0 -1
- package/dist/dashboard/out/_next/static/wPgKJtcOmTFLpUncDg16A/_ssgManifest.js +0 -1
- /package/dist/dashboard/out/_next/static/{wPgKJtcOmTFLpUncDg16A → sDcbGRTYLcpPvyTs_rsNb}/_buildManifest.js +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intro Expiration Service
|
|
3
|
+
*
|
|
4
|
+
* Handles auto-resize of workspaces when the free tier introductory period expires.
|
|
5
|
+
* Free users get Pro-level resources (2 CPU / 4GB) for the first 14 days,
|
|
6
|
+
* then get automatically downsized to standard free tier (1 CPU / 2GB).
|
|
7
|
+
*/
|
|
8
|
+
import { db } from '../db/index.js';
|
|
9
|
+
import { getProvisioner } from '../provisioner/index.js';
|
|
10
|
+
export const INTRO_PERIOD_DAYS = 14;
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
checkIntervalMs: 60 * 60 * 1000, // 1 hour
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Get intro period status for a user
|
|
17
|
+
*/
|
|
18
|
+
export function getIntroStatus(userCreatedAt, plan) {
|
|
19
|
+
const introPeriodDays = INTRO_PERIOD_DAYS;
|
|
20
|
+
// Only free tier users get intro bonus
|
|
21
|
+
if (plan !== 'free' || !userCreatedAt) {
|
|
22
|
+
return {
|
|
23
|
+
isIntroPeriod: false,
|
|
24
|
+
daysRemaining: 0,
|
|
25
|
+
introPeriodDays,
|
|
26
|
+
expiresAt: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const createdAt = typeof userCreatedAt === 'string' ? new Date(userCreatedAt) : userCreatedAt;
|
|
30
|
+
const daysSinceSignup = (Date.now() - createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
31
|
+
const isIntroPeriod = daysSinceSignup < introPeriodDays;
|
|
32
|
+
const daysRemaining = Math.max(0, Math.ceil(introPeriodDays - daysSinceSignup));
|
|
33
|
+
const expiresAt = new Date(createdAt.getTime() + introPeriodDays * 24 * 60 * 60 * 1000);
|
|
34
|
+
return {
|
|
35
|
+
isIntroPeriod,
|
|
36
|
+
daysRemaining,
|
|
37
|
+
introPeriodDays,
|
|
38
|
+
expiresAt,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export class IntroExpirationService {
|
|
42
|
+
config;
|
|
43
|
+
checkTimer = null;
|
|
44
|
+
isRunning = false;
|
|
45
|
+
constructor(config = {}) {
|
|
46
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start the expiration service
|
|
50
|
+
*/
|
|
51
|
+
start() {
|
|
52
|
+
if (!this.config.enabled) {
|
|
53
|
+
console.log('[intro-expiration] Service disabled');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (this.isRunning) {
|
|
57
|
+
console.warn('[intro-expiration] Service already running');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.isRunning = true;
|
|
61
|
+
console.log(`[intro-expiration] Started (checking every ${this.config.checkIntervalMs / 1000 / 60} minutes)`);
|
|
62
|
+
// Run immediately on start
|
|
63
|
+
this.runExpirationCheck().catch((err) => {
|
|
64
|
+
console.error('[intro-expiration] Initial run failed:', err);
|
|
65
|
+
});
|
|
66
|
+
// Then run periodically
|
|
67
|
+
this.checkTimer = setInterval(() => {
|
|
68
|
+
this.runExpirationCheck().catch((err) => {
|
|
69
|
+
console.error('[intro-expiration] Periodic run failed:', err);
|
|
70
|
+
});
|
|
71
|
+
}, this.config.checkIntervalMs);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stop the expiration service
|
|
75
|
+
*/
|
|
76
|
+
stop() {
|
|
77
|
+
if (this.checkTimer) {
|
|
78
|
+
clearInterval(this.checkTimer);
|
|
79
|
+
this.checkTimer = null;
|
|
80
|
+
}
|
|
81
|
+
this.isRunning = false;
|
|
82
|
+
console.log('[intro-expiration] Stopped');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Run expiration check for all free tier users with expired intro periods
|
|
86
|
+
*/
|
|
87
|
+
async runExpirationCheck() {
|
|
88
|
+
const results = [];
|
|
89
|
+
try {
|
|
90
|
+
// Get all users on free tier
|
|
91
|
+
const freeUsers = await db.users.findByPlan('free');
|
|
92
|
+
// Filter to users whose intro period has expired
|
|
93
|
+
const expiredUsers = freeUsers.filter((user) => {
|
|
94
|
+
const status = getIntroStatus(user.createdAt, user.plan || 'free');
|
|
95
|
+
return !status.isIntroPeriod && status.expiresAt !== null;
|
|
96
|
+
});
|
|
97
|
+
if (expiredUsers.length === 0) {
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
console.log(`[intro-expiration] Checking ${expiredUsers.length} users with expired intro periods`);
|
|
101
|
+
for (const user of expiredUsers) {
|
|
102
|
+
try {
|
|
103
|
+
const userResults = await this.checkAndResizeUserWorkspaces(user.id);
|
|
104
|
+
results.push(...userResults);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`[intro-expiration] Error checking user ${user.id}:`, err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Summary
|
|
111
|
+
const resized = results.filter((r) => r.action === 'resized').length;
|
|
112
|
+
const skipped = results.filter((r) => r.action === 'skipped').length;
|
|
113
|
+
const errors = results.filter((r) => r.action === 'error').length;
|
|
114
|
+
if (resized > 0 || errors > 0) {
|
|
115
|
+
console.log(`[intro-expiration] Results: ${resized} resized, ${skipped} skipped, ${errors} errors`);
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error('[intro-expiration] Run failed:', err);
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check and resize workspaces for a user whose intro period has expired
|
|
126
|
+
*/
|
|
127
|
+
async checkAndResizeUserWorkspaces(userId) {
|
|
128
|
+
const results = [];
|
|
129
|
+
const provisioner = getProvisioner();
|
|
130
|
+
// Get user's workspaces
|
|
131
|
+
const workspaces = await db.workspaces.findByUserId(userId);
|
|
132
|
+
for (const workspace of workspaces) {
|
|
133
|
+
try {
|
|
134
|
+
// Check if workspace has intro-sized resources
|
|
135
|
+
// We detect this by checking if it has 4GB memory (intro size) vs 2GB (standard)
|
|
136
|
+
const config = workspace.config;
|
|
137
|
+
const resourceTier = config?.resourceTier;
|
|
138
|
+
// Skip if already resized to standard free tier
|
|
139
|
+
// Intro workspaces would have been provisioned with medium tier (4GB)
|
|
140
|
+
// Standard free tier is small (2GB)
|
|
141
|
+
if (resourceTier === 'small') {
|
|
142
|
+
results.push({
|
|
143
|
+
userId,
|
|
144
|
+
workspaceId: workspace.id,
|
|
145
|
+
workspaceName: workspace.name,
|
|
146
|
+
action: 'skipped',
|
|
147
|
+
reason: 'Already at standard free tier size',
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Skip if workspace is not running (resize happens on next start)
|
|
152
|
+
if (workspace.status !== 'running') {
|
|
153
|
+
results.push({
|
|
154
|
+
userId,
|
|
155
|
+
workspaceId: workspace.id,
|
|
156
|
+
workspaceName: workspace.name,
|
|
157
|
+
action: 'skipped',
|
|
158
|
+
reason: `Workspace status is ${workspace.status}`,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Resize to standard free tier (small: 1 CPU / 2GB)
|
|
163
|
+
// Use skipRestart=true to not disrupt running agents
|
|
164
|
+
// The config is saved and will apply on next restart
|
|
165
|
+
console.log(`[intro-expiration] Resizing workspace ${workspace.name} to standard free tier`);
|
|
166
|
+
await provisioner.resize(workspace.id, {
|
|
167
|
+
name: 'small',
|
|
168
|
+
cpuCores: 1,
|
|
169
|
+
cpuKind: 'shared',
|
|
170
|
+
memoryMb: 2048,
|
|
171
|
+
maxAgents: 2,
|
|
172
|
+
}, true); // skipRestart = true for graceful resize
|
|
173
|
+
results.push({
|
|
174
|
+
userId,
|
|
175
|
+
workspaceId: workspace.id,
|
|
176
|
+
workspaceName: workspace.name,
|
|
177
|
+
action: 'resized',
|
|
178
|
+
reason: 'Intro period expired, downsized to standard free tier (applies on next restart)',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.error(`[intro-expiration] Failed to resize workspace ${workspace.id}:`, err);
|
|
183
|
+
results.push({
|
|
184
|
+
userId,
|
|
185
|
+
workspaceId: workspace.id,
|
|
186
|
+
workspaceName: workspace.name,
|
|
187
|
+
action: 'error',
|
|
188
|
+
reason: err instanceof Error ? err.message : 'Unknown error',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Singleton instance
|
|
196
|
+
let _service = null;
|
|
197
|
+
export function getIntroExpirationService() {
|
|
198
|
+
if (!_service) {
|
|
199
|
+
_service = new IntroExpirationService();
|
|
200
|
+
}
|
|
201
|
+
return _service;
|
|
202
|
+
}
|
|
203
|
+
export function startIntroExpirationService() {
|
|
204
|
+
getIntroExpirationService().start();
|
|
205
|
+
}
|
|
206
|
+
export function stopIntroExpirationService() {
|
|
207
|
+
if (_service) {
|
|
208
|
+
_service.stop();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=intro-expiration.js.map
|
|
@@ -174,6 +174,20 @@ declare class NangoService {
|
|
|
174
174
|
}>;
|
|
175
175
|
hasMore: boolean;
|
|
176
176
|
}>;
|
|
177
|
+
/**
|
|
178
|
+
* List collaborators for a repository via Nango Proxy.
|
|
179
|
+
* Uses the GitHub App connection to access repository collaborators.
|
|
180
|
+
* @param connectionId - GitHub App connection ID
|
|
181
|
+
* @param owner - Repository owner
|
|
182
|
+
* @param repo - Repository name
|
|
183
|
+
* @returns List of collaborators with their permissions
|
|
184
|
+
*/
|
|
185
|
+
listRepoCollaborators(connectionId: string, owner: string, repo: string): Promise<Array<{
|
|
186
|
+
id: number;
|
|
187
|
+
login: string;
|
|
188
|
+
avatarUrl: string;
|
|
189
|
+
permission: 'admin' | 'write' | 'read' | 'none';
|
|
190
|
+
}>>;
|
|
177
191
|
/**
|
|
178
192
|
* Verify webhook signature sent by Nango.
|
|
179
193
|
* Uses the new verifyIncomingWebhookRequest method.
|
|
@@ -46,23 +46,35 @@ class NangoService {
|
|
|
46
46
|
* For API calls, use the proxy methods instead.
|
|
47
47
|
*/
|
|
48
48
|
async getGithubAppToken(connectionId) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Nango may return an object with access_token
|
|
55
|
-
if (token && typeof token === 'object') {
|
|
56
|
-
const tokenObj = token;
|
|
57
|
-
if (tokenObj.access_token) {
|
|
58
|
-
return tokenObj.access_token;
|
|
49
|
+
try {
|
|
50
|
+
const token = await this.client.getToken(NANGO_INTEGRATIONS.GITHUB_APP, connectionId, false, true);
|
|
51
|
+
// Handle different return formats from Nango
|
|
52
|
+
if (typeof token === 'string') {
|
|
53
|
+
return token;
|
|
59
54
|
}
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// Nango may return an object with access_token
|
|
56
|
+
if (token && typeof token === 'object') {
|
|
57
|
+
const tokenObj = token;
|
|
58
|
+
if (tokenObj.access_token) {
|
|
59
|
+
return tokenObj.access_token;
|
|
60
|
+
}
|
|
61
|
+
if (tokenObj.token) {
|
|
62
|
+
return tokenObj.token;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.error('[nango] Unexpected token format:', typeof token, token);
|
|
66
|
+
throw new Error('Expected GitHub App token to be a string');
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Handle 404 (connection not found) gracefully
|
|
70
|
+
const error = err;
|
|
71
|
+
const status = error.response?.status || error.status;
|
|
72
|
+
if (status === 404) {
|
|
73
|
+
throw new Error(`GitHub App connection not found: ${connectionId} (may have been disconnected)`);
|
|
62
74
|
}
|
|
75
|
+
// Re-throw with cleaner message
|
|
76
|
+
throw new Error(`Failed to get GitHub App token: ${err.message || 'Unknown error'}`);
|
|
63
77
|
}
|
|
64
|
-
console.error('[nango] Unexpected token format:', typeof token, token);
|
|
65
|
-
throw new Error('Expected GitHub App token to be a string');
|
|
66
78
|
}
|
|
67
79
|
/**
|
|
68
80
|
* Retrieve the user's OAuth access token from a GitHub App OAuth connection.
|
|
@@ -315,6 +327,54 @@ class NangoService {
|
|
|
315
327
|
hasMore: repos.length === perPage,
|
|
316
328
|
};
|
|
317
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* List collaborators for a repository via Nango Proxy.
|
|
332
|
+
* Uses the GitHub App connection to access repository collaborators.
|
|
333
|
+
* @param connectionId - GitHub App connection ID
|
|
334
|
+
* @param owner - Repository owner
|
|
335
|
+
* @param repo - Repository name
|
|
336
|
+
* @returns List of collaborators with their permissions
|
|
337
|
+
*/
|
|
338
|
+
async listRepoCollaborators(connectionId, owner, repo) {
|
|
339
|
+
try {
|
|
340
|
+
const response = await this.client.get({
|
|
341
|
+
connectionId,
|
|
342
|
+
providerConfigKey: NANGO_INTEGRATIONS.GITHUB_APP,
|
|
343
|
+
endpoint: `/repos/${owner}/${repo}/collaborators`,
|
|
344
|
+
params: { per_page: '100' },
|
|
345
|
+
});
|
|
346
|
+
return (response.data || []).map(collab => {
|
|
347
|
+
let permission = 'none';
|
|
348
|
+
if (collab.permissions) {
|
|
349
|
+
if (collab.permissions.admin) {
|
|
350
|
+
permission = 'admin';
|
|
351
|
+
}
|
|
352
|
+
else if (collab.permissions.push) {
|
|
353
|
+
permission = 'write';
|
|
354
|
+
}
|
|
355
|
+
else if (collab.permissions.pull) {
|
|
356
|
+
permission = 'read';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
id: collab.id,
|
|
361
|
+
login: collab.login,
|
|
362
|
+
avatarUrl: collab.avatar_url,
|
|
363
|
+
permission,
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
const error = err;
|
|
369
|
+
// 403 = no permission to view collaborators, 404 = repo not found
|
|
370
|
+
if (error.response?.status === 403 || error.response?.status === 404) {
|
|
371
|
+
console.warn(`[nango] Cannot list collaborators for ${owner}/${repo}: status ${error.response.status}`);
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
console.error('[nango] listRepoCollaborators error:', err);
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
318
378
|
/**
|
|
319
379
|
* Verify webhook signature sent by Nango.
|
|
320
380
|
* Uses the new verifyIncomingWebhookRequest method.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Security Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides secure SSH password derivation for workspace containers.
|
|
5
|
+
* Uses a deterministic approach based on workspace ID + secret salt,
|
|
6
|
+
* ensuring each workspace has a unique password without storage.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Derive a unique SSH password for a workspace.
|
|
10
|
+
*
|
|
11
|
+
* Uses SHA-256 hash of (workspaceId + salt) to generate a deterministic
|
|
12
|
+
* but unique password for each workspace. This approach:
|
|
13
|
+
* - Ensures each workspace has a unique password
|
|
14
|
+
* - Requires no database storage
|
|
15
|
+
* - Produces consistent results across cloud server and container
|
|
16
|
+
*
|
|
17
|
+
* SECURITY: Set SSH_PASSWORD_SALT environment variable in production!
|
|
18
|
+
* The default salt is insecure and should never be used in production.
|
|
19
|
+
*
|
|
20
|
+
* @param workspaceId - The workspace UUID
|
|
21
|
+
* @returns A 24-character hex password (96 bits of entropy)
|
|
22
|
+
*/
|
|
23
|
+
export declare function deriveSshPassword(workspaceId: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Validate that SSH security is properly configured.
|
|
26
|
+
* Call this at server startup to catch configuration issues early.
|
|
27
|
+
*
|
|
28
|
+
* @returns true if properly configured, false otherwise
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateSshSecurityConfig(): boolean;
|
|
31
|
+
//# sourceMappingURL=ssh-security.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Security Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides secure SSH password derivation for workspace containers.
|
|
5
|
+
* Uses a deterministic approach based on workspace ID + secret salt,
|
|
6
|
+
* ensuring each workspace has a unique password without storage.
|
|
7
|
+
*/
|
|
8
|
+
import * as crypto from 'crypto';
|
|
9
|
+
const DEFAULT_SALT = 'default-salt-change-in-prod';
|
|
10
|
+
/**
|
|
11
|
+
* Derive a unique SSH password for a workspace.
|
|
12
|
+
*
|
|
13
|
+
* Uses SHA-256 hash of (workspaceId + salt) to generate a deterministic
|
|
14
|
+
* but unique password for each workspace. This approach:
|
|
15
|
+
* - Ensures each workspace has a unique password
|
|
16
|
+
* - Requires no database storage
|
|
17
|
+
* - Produces consistent results across cloud server and container
|
|
18
|
+
*
|
|
19
|
+
* SECURITY: Set SSH_PASSWORD_SALT environment variable in production!
|
|
20
|
+
* The default salt is insecure and should never be used in production.
|
|
21
|
+
*
|
|
22
|
+
* @param workspaceId - The workspace UUID
|
|
23
|
+
* @returns A 24-character hex password (96 bits of entropy)
|
|
24
|
+
*/
|
|
25
|
+
export function deriveSshPassword(workspaceId) {
|
|
26
|
+
const salt = process.env.SSH_PASSWORD_SALT;
|
|
27
|
+
// Warn if using default salt in production
|
|
28
|
+
if (!salt) {
|
|
29
|
+
const isProduction = process.env.NODE_ENV === 'production' || process.env.FLY_APP_NAME;
|
|
30
|
+
if (isProduction) {
|
|
31
|
+
console.warn('[SECURITY WARNING] SSH_PASSWORD_SALT is not set! ' +
|
|
32
|
+
'Using default salt is INSECURE in production. ' +
|
|
33
|
+
'Set SSH_PASSWORD_SALT to a random 32+ character secret.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const effectiveSalt = salt || DEFAULT_SALT;
|
|
37
|
+
return crypto
|
|
38
|
+
.createHash('sha256')
|
|
39
|
+
.update(`${workspaceId}:${effectiveSalt}`)
|
|
40
|
+
.digest('hex')
|
|
41
|
+
.substring(0, 24); // 24 hex chars = 96 bits of entropy
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate that SSH security is properly configured.
|
|
45
|
+
* Call this at server startup to catch configuration issues early.
|
|
46
|
+
*
|
|
47
|
+
* @returns true if properly configured, false otherwise
|
|
48
|
+
*/
|
|
49
|
+
export function validateSshSecurityConfig() {
|
|
50
|
+
const salt = process.env.SSH_PASSWORD_SALT;
|
|
51
|
+
const isProduction = process.env.NODE_ENV === 'production' || process.env.FLY_APP_NAME;
|
|
52
|
+
if (isProduction && !salt) {
|
|
53
|
+
console.error('[SECURITY ERROR] SSH_PASSWORD_SALT must be set in production! ' +
|
|
54
|
+
'Generate one with: openssl rand -hex 32');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (salt && salt.length < 16) {
|
|
58
|
+
console.warn('[SECURITY WARNING] SSH_PASSWORD_SALT should be at least 16 characters. ' +
|
|
59
|
+
'Current length: ' + salt.length);
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=ssh-security.js.map
|
|
@@ -102,6 +102,11 @@ export declare class ContinuityManager {
|
|
|
102
102
|
* Filter placeholder values from a handoff (defensive)
|
|
103
103
|
*/
|
|
104
104
|
private filterHandoffPlaceholders;
|
|
105
|
+
/**
|
|
106
|
+
* Filter placeholder values from ledger updates (for object input to saveLedger).
|
|
107
|
+
* This ensures placeholder values like "...", "task1", etc. don't get saved.
|
|
108
|
+
*/
|
|
109
|
+
private filterUpdatesPlaceholders;
|
|
105
110
|
/**
|
|
106
111
|
* Format a ledger for display/injection
|
|
107
112
|
*/
|
|
@@ -91,8 +91,16 @@ export class ContinuityManager {
|
|
|
91
91
|
*/
|
|
92
92
|
async saveLedger(agentName, content, options = {}) {
|
|
93
93
|
await this.initialize();
|
|
94
|
-
// Parse content if string
|
|
95
|
-
|
|
94
|
+
// Parse content if string, otherwise filter placeholders from object input
|
|
95
|
+
// This ensures placeholder values like "...", "task1", etc. never get saved
|
|
96
|
+
let updates;
|
|
97
|
+
if (typeof content === 'string') {
|
|
98
|
+
updates = parseSaveContent(content);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Filter placeholders from object input (e.g., from [[SUMMARY]] blocks)
|
|
102
|
+
updates = this.filterUpdatesPlaceholders(content);
|
|
103
|
+
}
|
|
96
104
|
// Get or create existing ledger
|
|
97
105
|
let ledger = await this.ledgerStore.load(agentName);
|
|
98
106
|
if (ledger) {
|
|
@@ -329,6 +337,52 @@ export class ContinuityManager {
|
|
|
329
337
|
learnings: handoff.learnings ? filterPlaceholders(handoff.learnings) : undefined,
|
|
330
338
|
};
|
|
331
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Filter placeholder values from ledger updates (for object input to saveLedger).
|
|
342
|
+
* This ensures placeholder values like "...", "task1", etc. don't get saved.
|
|
343
|
+
*/
|
|
344
|
+
filterUpdatesPlaceholders(updates) {
|
|
345
|
+
const filtered = { ...updates };
|
|
346
|
+
// Filter string fields
|
|
347
|
+
if (filtered.currentTask !== undefined) {
|
|
348
|
+
filtered.currentTask = isPlaceholderValue(filtered.currentTask) ? undefined : filtered.currentTask;
|
|
349
|
+
if (filtered.currentTask === undefined)
|
|
350
|
+
delete filtered.currentTask;
|
|
351
|
+
}
|
|
352
|
+
// Filter array fields
|
|
353
|
+
if (filtered.completed) {
|
|
354
|
+
filtered.completed = filterPlaceholders(filtered.completed);
|
|
355
|
+
if (filtered.completed.length === 0)
|
|
356
|
+
delete filtered.completed;
|
|
357
|
+
}
|
|
358
|
+
if (filtered.inProgress) {
|
|
359
|
+
filtered.inProgress = filterPlaceholders(filtered.inProgress);
|
|
360
|
+
if (filtered.inProgress.length === 0)
|
|
361
|
+
delete filtered.inProgress;
|
|
362
|
+
}
|
|
363
|
+
if (filtered.blocked) {
|
|
364
|
+
filtered.blocked = filterPlaceholders(filtered.blocked);
|
|
365
|
+
if (filtered.blocked.length === 0)
|
|
366
|
+
delete filtered.blocked;
|
|
367
|
+
}
|
|
368
|
+
if (filtered.uncertainItems) {
|
|
369
|
+
filtered.uncertainItems = filterPlaceholders(filtered.uncertainItems);
|
|
370
|
+
if (filtered.uncertainItems.length === 0)
|
|
371
|
+
delete filtered.uncertainItems;
|
|
372
|
+
}
|
|
373
|
+
// Filter complex array fields
|
|
374
|
+
if (filtered.fileContext) {
|
|
375
|
+
filtered.fileContext = filtered.fileContext.filter(f => !isPlaceholderValue(f.path));
|
|
376
|
+
if (filtered.fileContext.length === 0)
|
|
377
|
+
delete filtered.fileContext;
|
|
378
|
+
}
|
|
379
|
+
if (filtered.keyDecisions) {
|
|
380
|
+
filtered.keyDecisions = filtered.keyDecisions.filter(d => !isPlaceholderValue(d.decision));
|
|
381
|
+
if (filtered.keyDecisions.length === 0)
|
|
382
|
+
delete filtered.keyDecisions;
|
|
383
|
+
}
|
|
384
|
+
return filtered;
|
|
385
|
+
}
|
|
332
386
|
/**
|
|
333
387
|
* Format a ledger for display/injection
|
|
334
388
|
*/
|
package/dist/daemon/api.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class DaemonApi extends EventEmitter {
|
|
|
14
14
|
private config;
|
|
15
15
|
private allowedOrigins;
|
|
16
16
|
private allowAllOrigins;
|
|
17
|
+
private clientAlive;
|
|
18
|
+
private pingInterval?;
|
|
17
19
|
constructor(config: ApiDaemonConfig);
|
|
18
20
|
/**
|
|
19
21
|
* Resolve allowed origins from config/env (comma-separated list).
|