@ash-ai/sandbox 0.0.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/dist/pool.js ADDED
@@ -0,0 +1,251 @@
1
+ import { DEFAULT_MAX_SANDBOXES, DEFAULT_IDLE_TIMEOUT_MS, IDLE_SWEEP_INTERVAL_MS } from '@ash-ai/shared';
2
+ import { deleteSessionState, deleteCloudState } from './state-persistence.js';
3
+ export class SandboxPool {
4
+ live = new Map();
5
+ sessionIndex = new Map(); // sessionId → sandboxId
6
+ manager;
7
+ db;
8
+ dataDir;
9
+ maxCapacity;
10
+ idleTimeoutMs;
11
+ onBeforeEvict;
12
+ sweepTimer = null;
13
+ resumeWarmHits = 0;
14
+ resumeColdHits = 0;
15
+ constructor(opts) {
16
+ this.manager = opts.manager;
17
+ this.db = opts.db;
18
+ this.dataDir = opts.dataDir;
19
+ this.maxCapacity = opts.maxCapacity ?? DEFAULT_MAX_SANDBOXES;
20
+ this.idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
21
+ this.onBeforeEvict = opts.onBeforeEvict;
22
+ }
23
+ // --- Lifecycle ---
24
+ async init() {
25
+ const marked = await this.db.markAllSandboxesCold();
26
+ if (marked > 0) {
27
+ console.log(`[pool] Startup: marked ${marked} stale sandbox(es) as cold`);
28
+ }
29
+ }
30
+ async create(opts) {
31
+ // Enforce capacity — evict if needed
32
+ const count = await this.db.countSandboxes();
33
+ if (count >= this.maxCapacity) {
34
+ const evicted = await this.evictOne();
35
+ if (!evicted) {
36
+ throw new Error('Sandbox capacity reached and no evictable sandboxes (all running)');
37
+ }
38
+ }
39
+ const sandbox = await this.manager.create(opts);
40
+ // Insert DB row directly as 'warm' — no intermediate warming state needed
41
+ // since the manager.create() call above is the only thing that could fail.
42
+ await this.db.insertSandbox(sandbox.id, opts.agentName, sandbox.workspaceDir, opts.sessionId);
43
+ await this.db.updateSandboxState(sandbox.id, 'warm');
44
+ // Cache in live map
45
+ const entry = {
46
+ sandbox,
47
+ state: 'warm',
48
+ sessionId: opts.sessionId,
49
+ agentName: opts.agentName,
50
+ };
51
+ this.live.set(sandbox.id, entry);
52
+ if (opts.sessionId) {
53
+ this.sessionIndex.set(opts.sessionId, sandbox.id);
54
+ }
55
+ return sandbox;
56
+ }
57
+ get(sandboxId) {
58
+ const entry = this.live.get(sandboxId);
59
+ if (!entry)
60
+ return undefined;
61
+ // Check if process is dead
62
+ if (entry.sandbox.process.exitCode !== null) {
63
+ this.live.delete(sandboxId);
64
+ if (entry.sessionId) {
65
+ this.sessionIndex.delete(entry.sessionId);
66
+ }
67
+ // Fire-and-forget DB update to cold
68
+ this.db.updateSandboxState(sandboxId, 'cold').catch((err) => console.error(`[pool] Failed to mark dead sandbox ${sandboxId} as cold:`, err));
69
+ return undefined;
70
+ }
71
+ return entry.sandbox;
72
+ }
73
+ getEntry(sandboxId) {
74
+ return this.live.get(sandboxId);
75
+ }
76
+ getSandboxForSession(sessionId) {
77
+ const sandboxId = this.sessionIndex.get(sessionId);
78
+ if (!sandboxId)
79
+ return undefined;
80
+ return this.get(sandboxId);
81
+ }
82
+ async destroy(sandboxId) {
83
+ const entry = this.live.get(sandboxId);
84
+ // Kill process via manager
85
+ await this.manager.destroy(sandboxId);
86
+ // Remove from in-memory maps
87
+ if (entry) {
88
+ this.live.delete(sandboxId);
89
+ if (entry.sessionId) {
90
+ this.sessionIndex.delete(entry.sessionId);
91
+ }
92
+ }
93
+ // Delete DB row
94
+ await this.db.deleteSandbox(sandboxId);
95
+ }
96
+ async destroyAll() {
97
+ const ids = [...this.live.keys()];
98
+ await Promise.all(ids.map((id) => this.destroy(id)));
99
+ // Also clean cold entries from DB
100
+ // (destroyAll is for full shutdown — clean everything)
101
+ }
102
+ // --- State transitions ---
103
+ markRunning(sandboxId) {
104
+ const entry = this.live.get(sandboxId);
105
+ if (!entry)
106
+ return;
107
+ entry.state = 'running';
108
+ // Fire-and-forget DB update
109
+ this.db.updateSandboxState(sandboxId, 'running').catch((err) => console.error(`[pool] Failed to update sandbox ${sandboxId} state to running:`, err));
110
+ this.db.touchSandbox(sandboxId).catch(() => { });
111
+ }
112
+ markWaiting(sandboxId) {
113
+ const entry = this.live.get(sandboxId);
114
+ if (!entry)
115
+ return;
116
+ entry.state = 'waiting';
117
+ // Fire-and-forget DB update
118
+ this.db.updateSandboxState(sandboxId, 'waiting').catch((err) => console.error(`[pool] Failed to update sandbox ${sandboxId} state to waiting:`, err));
119
+ this.db.touchSandbox(sandboxId).catch(() => { });
120
+ }
121
+ // --- Eviction ---
122
+ async evictOne() {
123
+ const candidate = await this.db.getBestEvictionCandidate();
124
+ if (!candidate)
125
+ return false;
126
+ if (candidate.state === 'cold') {
127
+ // Cold eviction — delete persisted state + DB row
128
+ if (candidate.sessionId) {
129
+ deleteSessionState(this.dataDir, candidate.sessionId);
130
+ deleteCloudState(candidate.sessionId).catch((err) => console.error(`[pool] Cloud delete failed for ${candidate.sessionId}:`, err));
131
+ }
132
+ await this.db.deleteSandbox(candidate.id);
133
+ return true;
134
+ }
135
+ if (candidate.state === 'warm') {
136
+ // Warm eviction — kill sandbox (no active session work)
137
+ await this.manager.destroy(candidate.id);
138
+ this.live.delete(candidate.id);
139
+ if (candidate.sessionId) {
140
+ this.sessionIndex.delete(candidate.sessionId);
141
+ }
142
+ await this.db.deleteSandbox(candidate.id);
143
+ return true;
144
+ }
145
+ // Waiting eviction — kill sandbox (idle session), preserve state
146
+ const entry = this.live.get(candidate.id);
147
+ if (entry && this.onBeforeEvict) {
148
+ await this.onBeforeEvict(entry);
149
+ }
150
+ await this.manager.destroy(candidate.id);
151
+ this.live.delete(candidate.id);
152
+ if (candidate.sessionId) {
153
+ this.sessionIndex.delete(candidate.sessionId);
154
+ }
155
+ // Mark cold (session is paused, state persisted by onBeforeEvict)
156
+ await this.db.updateSandboxState(candidate.id, 'cold');
157
+ return true;
158
+ }
159
+ // --- Idle sweep ---
160
+ async sweepIdle() {
161
+ const threshold = new Date(Date.now() - this.idleTimeoutMs).toISOString();
162
+ const idleSandboxes = await this.db.getIdleSandboxes(threshold);
163
+ let swept = 0;
164
+ for (const record of idleSandboxes) {
165
+ const entry = this.live.get(record.id);
166
+ if (!entry)
167
+ continue; // not live — skip
168
+ // Evict: persist state, kill, mark cold
169
+ if (this.onBeforeEvict) {
170
+ await this.onBeforeEvict(entry);
171
+ }
172
+ await this.manager.destroy(record.id);
173
+ this.live.delete(record.id);
174
+ if (entry.sessionId) {
175
+ this.sessionIndex.delete(entry.sessionId);
176
+ }
177
+ await this.db.updateSandboxState(record.id, 'cold');
178
+ swept++;
179
+ }
180
+ if (swept > 0) {
181
+ console.log(`[pool] Idle sweep: evicted ${swept} sandbox(es)`);
182
+ }
183
+ return swept;
184
+ }
185
+ startIdleSweep() {
186
+ if (this.sweepTimer)
187
+ return;
188
+ this.sweepTimer = setInterval(() => {
189
+ this.sweepIdle().catch((err) => console.error('[pool] Idle sweep error:', err));
190
+ }, IDLE_SWEEP_INTERVAL_MS);
191
+ // Unref so the timer doesn't keep the process alive
192
+ this.sweepTimer.unref();
193
+ }
194
+ stopIdleSweep() {
195
+ if (this.sweepTimer) {
196
+ clearInterval(this.sweepTimer);
197
+ this.sweepTimer = null;
198
+ }
199
+ }
200
+ // --- Resume metrics ---
201
+ recordWarmHit() { this.resumeWarmHits++; }
202
+ recordColdHit() { this.resumeColdHits++; }
203
+ // --- Stats ---
204
+ get stats() {
205
+ // Count live states from in-memory map
206
+ let warming = 0, warm = 0, waiting = 0, running = 0;
207
+ for (const entry of this.live.values()) {
208
+ switch (entry.state) {
209
+ case 'warming':
210
+ warming++;
211
+ break;
212
+ case 'warm':
213
+ warm++;
214
+ break;
215
+ case 'waiting':
216
+ waiting++;
217
+ break;
218
+ case 'running':
219
+ running++;
220
+ break;
221
+ }
222
+ }
223
+ // We can't synchronously get cold count from DB, so we compute total from what we know.
224
+ // For accurate cold count, use statsAsync().
225
+ return {
226
+ total: this.live.size, // live only — call statsAsync for full count
227
+ cold: 0,
228
+ warming,
229
+ warm,
230
+ waiting,
231
+ running,
232
+ maxCapacity: this.maxCapacity,
233
+ resumeWarmHits: this.resumeWarmHits,
234
+ resumeColdHits: this.resumeColdHits,
235
+ };
236
+ }
237
+ async statsAsync() {
238
+ const baseStats = this.stats;
239
+ const totalDb = await this.db.countSandboxes();
240
+ const cold = totalDb - this.live.size;
241
+ return {
242
+ ...baseStats,
243
+ total: totalDb,
244
+ cold: Math.max(0, cold),
245
+ };
246
+ }
247
+ get activeCount() {
248
+ return this.live.size;
249
+ }
250
+ }
251
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
2
+ import type { SandboxLimits } from '@ash-ai/shared';
3
+ import { DEFAULT_SANDBOX_LIMITS } from '@ash-ai/shared';
4
+ export { DEFAULT_SANDBOX_LIMITS };
5
+ export interface SpawnResult {
6
+ child: ChildProcess;
7
+ cleanup: () => void;
8
+ }
9
+ export interface SandboxSpawnOpts {
10
+ sandboxId: string;
11
+ workspaceDir: string;
12
+ agentDir: string;
13
+ }
14
+ export declare function createCgroup(sandboxId: string, limits: SandboxLimits): string;
15
+ export declare function addToCgroup(cgroupPath: string, pid: number): void;
16
+ export declare function removeCgroup(cgroupPath: string): void;
17
+ export declare function spawnWithLimits(command: string, args: string[], opts: SpawnOptions, limits: SandboxLimits, sandboxOpts: SandboxSpawnOpts): SpawnResult;
18
+ export declare function isOomExit(code: number | null, signal: string | null): boolean;
19
+ export declare function getDirSizeKb(dir: string): number;
20
+ export declare function startDiskMonitor(workspaceDir: string, limitMb: number, onExceeded: () => void, intervalMs?: number): NodeJS.Timeout;
21
+ //# sourceMappingURL=resource-limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-limits.d.ts","sourceRoot":"","sources":["../src/resource-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG3F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAA0B,MAAM,gBAAgB,CAAC;AAEhF,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAMlC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,CAqB7E;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjE;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAMrD;AA6CD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,gBAAgB,GAC5B,WAAW,CAOb;AAmCD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAE7E;AAMD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhD;AAED,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,IAAI,EACtB,UAAU,GAAE,MAA+B,GAC1C,MAAM,CAAC,OAAO,CAWhB"}
@@ -0,0 +1,139 @@
1
+ import { execSync, spawn } from 'node:child_process';
2
+ import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { DEFAULT_SANDBOX_LIMITS, DISK_CHECK_INTERVAL_MS } from '@ash-ai/shared';
5
+ export { DEFAULT_SANDBOX_LIMITS };
6
+ // =============================================================================
7
+ // Platform detection
8
+ // =============================================================================
9
+ function hasCgroups() {
10
+ if (process.platform !== 'linux')
11
+ return false;
12
+ try {
13
+ // Check that the ash cgroup parent exists and is writable
14
+ // (docker-entrypoint.sh sets this up, or it exists on native Linux with proper perms)
15
+ execSync('test -d /sys/fs/cgroup/ash && test -w /sys/fs/cgroup/ash', { stdio: 'ignore' });
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ // =============================================================================
23
+ // Linux: cgroups v2
24
+ // =============================================================================
25
+ const CGROUP_ROOT = '/sys/fs/cgroup/ash';
26
+ export function createCgroup(sandboxId, limits) {
27
+ const cgroupPath = join(CGROUP_ROOT, sandboxId);
28
+ mkdirSync(cgroupPath, { recursive: true });
29
+ // Memory limit
30
+ const memoryBytes = limits.memoryMb * 1024 * 1024;
31
+ writeFileSync(join(cgroupPath, 'memory.max'), String(memoryBytes));
32
+ try {
33
+ writeFileSync(join(cgroupPath, 'memory.swap.max'), '0');
34
+ }
35
+ catch {
36
+ // swap controller may not be enabled
37
+ }
38
+ // CPU limit (100000 period, quota scales with cpuPercent)
39
+ const cpuQuota = limits.cpuPercent * 1000;
40
+ writeFileSync(join(cgroupPath, 'cpu.max'), `${cpuQuota} 100000`);
41
+ // Process limit (fork bomb protection)
42
+ writeFileSync(join(cgroupPath, 'pids.max'), String(limits.maxProcesses));
43
+ return cgroupPath;
44
+ }
45
+ export function addToCgroup(cgroupPath, pid) {
46
+ writeFileSync(join(cgroupPath, 'cgroup.procs'), String(pid));
47
+ }
48
+ export function removeCgroup(cgroupPath) {
49
+ try {
50
+ rmSync(cgroupPath, { recursive: true, force: true });
51
+ }
52
+ catch {
53
+ // may already be gone or processes still inside
54
+ }
55
+ }
56
+ // =============================================================================
57
+ // Fallback: ulimit (macOS dev without Docker)
58
+ // =============================================================================
59
+ function buildUlimitPrefix(limits) {
60
+ const parts = [];
61
+ // ulimit -v (virtual memory) doesn't work on macOS — skip it there
62
+ if (process.platform !== 'darwin') {
63
+ parts.push(`ulimit -v ${limits.memoryMb * 1024}`);
64
+ }
65
+ // ulimit -u (max user processes) — on macOS this applies to the entire user,
66
+ // not just the subprocess tree. Setting it to 64 when the user already has 60+
67
+ // processes causes "fork: Resource temporarily unavailable". Skip on macOS.
68
+ if (process.platform !== 'darwin') {
69
+ parts.push(`ulimit -u ${limits.maxProcesses}`);
70
+ }
71
+ parts.push(`ulimit -f ${limits.diskMb * 1024}`); // Max file size (KB blocks)
72
+ return parts.join(' && ');
73
+ }
74
+ function spawnWithUlimit(command, args, opts, limits) {
75
+ const prefix = buildUlimitPrefix(limits);
76
+ const escapedArgs = args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
77
+ // Use bash (not sh) — Debian's sh is dash which doesn't support ulimit -u
78
+ const shell = process.platform === 'linux' ? 'bash' : 'sh';
79
+ const child = spawn(shell, ['-c', `${prefix} && exec ${command} ${escapedArgs}`], opts);
80
+ return { child, cleanup: () => { } };
81
+ }
82
+ // =============================================================================
83
+ // Unified spawn
84
+ // =============================================================================
85
+ export function spawnWithLimits(command, args, opts, limits, sandboxOpts) {
86
+ if (hasCgroups()) {
87
+ return spawnWithCgroups(command, args, opts, limits, sandboxOpts);
88
+ }
89
+ // macOS or unprivileged Linux — ulimit fallback
90
+ return spawnWithUlimit(command, args, opts, limits);
91
+ }
92
+ function spawnWithCgroups(command, args, opts, limits, sandboxOpts) {
93
+ let cgroupPath;
94
+ try {
95
+ cgroupPath = createCgroup(sandboxOpts.sandboxId, limits);
96
+ }
97
+ catch (err) {
98
+ console.error(`[resource-limits] cgroups available but failed to create: ${err}`);
99
+ return spawnWithUlimit(command, args, opts, limits);
100
+ }
101
+ const child = spawn(command, args, opts);
102
+ if (child.pid) {
103
+ try {
104
+ addToCgroup(cgroupPath, child.pid);
105
+ }
106
+ catch (err) {
107
+ console.error(`[resource-limits] Failed to add PID ${child.pid} to cgroup: ${err}`);
108
+ }
109
+ }
110
+ const cleanup = () => removeCgroup(cgroupPath);
111
+ return { child, cleanup };
112
+ }
113
+ // =============================================================================
114
+ // OOM detection
115
+ // =============================================================================
116
+ export function isOomExit(code, signal) {
117
+ return signal === 'SIGKILL' || code === 137;
118
+ }
119
+ // =============================================================================
120
+ // Disk usage monitoring
121
+ // =============================================================================
122
+ export function getDirSizeKb(dir) {
123
+ const output = execSync(`du -sk '${dir}'`, { timeout: 5000 }).toString().trim();
124
+ return parseInt(output.split('\t')[0], 10);
125
+ }
126
+ export function startDiskMonitor(workspaceDir, limitMb, onExceeded, intervalMs = DISK_CHECK_INTERVAL_MS) {
127
+ return setInterval(() => {
128
+ try {
129
+ const sizeKb = getDirSizeKb(workspaceDir);
130
+ if (sizeKb > limitMb * 1024) {
131
+ onExceeded();
132
+ }
133
+ }
134
+ catch {
135
+ // du failed — workspace may be gone
136
+ }
137
+ }, intervalMs);
138
+ }
139
+ //# sourceMappingURL=resource-limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-limits.js","sourceRoot":"","sources":["../src/resource-limits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { SnapshotStore } from './snapshot-store.js';
2
+ export declare class GcsSnapshotStore implements SnapshotStore {
3
+ private storage;
4
+ private bucket;
5
+ private prefix;
6
+ constructor(bucket: string, prefix: string);
7
+ private key;
8
+ upload(sessionId: string, tarPath: string): Promise<boolean>;
9
+ download(sessionId: string, destPath: string): Promise<boolean>;
10
+ exists(sessionId: string): Promise<boolean>;
11
+ delete(sessionId: string): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=snapshot-gcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-gcs.d.ts","sourceRoot":"","sources":["../src/snapshot-gcs.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,qBAAa,gBAAiB,YAAW,aAAa;IACpD,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAM1C,OAAO,CAAC,GAAG;IAIL,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY5D,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc/D,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAU3C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ/C"}
@@ -0,0 +1,63 @@
1
+ import { Storage } from '@google-cloud/storage';
2
+ import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { pipeline } from 'node:stream/promises';
4
+ export class GcsSnapshotStore {
5
+ storage;
6
+ bucket;
7
+ prefix;
8
+ constructor(bucket, prefix) {
9
+ this.bucket = bucket;
10
+ this.prefix = prefix;
11
+ this.storage = new Storage(); // Uses ADC (Application Default Credentials)
12
+ }
13
+ key(sessionId) {
14
+ return `${this.prefix}${sessionId}/workspace.tar.gz`;
15
+ }
16
+ async upload(sessionId, tarPath) {
17
+ try {
18
+ const file = this.storage.bucket(this.bucket).file(this.key(sessionId));
19
+ const stream = createReadStream(tarPath);
20
+ await pipeline(stream, file.createWriteStream({ contentType: 'application/gzip' }));
21
+ return true;
22
+ }
23
+ catch (err) {
24
+ console.error(`[snapshot-gcs] Upload failed for ${sessionId}:`, err);
25
+ return false;
26
+ }
27
+ }
28
+ async download(sessionId, destPath) {
29
+ try {
30
+ const file = this.storage.bucket(this.bucket).file(this.key(sessionId));
31
+ const [exists] = await file.exists();
32
+ if (!exists)
33
+ return false;
34
+ const ws = createWriteStream(destPath);
35
+ await pipeline(file.createReadStream(), ws);
36
+ return true;
37
+ }
38
+ catch (err) {
39
+ console.error(`[snapshot-gcs] Download failed for ${sessionId}:`, err);
40
+ return false;
41
+ }
42
+ }
43
+ async exists(sessionId) {
44
+ try {
45
+ const file = this.storage.bucket(this.bucket).file(this.key(sessionId));
46
+ const [exists] = await file.exists();
47
+ return exists;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async delete(sessionId) {
54
+ try {
55
+ const file = this.storage.bucket(this.bucket).file(this.key(sessionId));
56
+ await file.delete({ ignoreNotFound: true });
57
+ }
58
+ catch (err) {
59
+ console.error(`[snapshot-gcs] Delete failed for ${sessionId}:`, err);
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=snapshot-gcs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-gcs.js","sourceRoot":"","sources":["../src/snapshot-gcs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { SnapshotStore } from './snapshot-store.js';
2
+ export declare class S3SnapshotStore implements SnapshotStore {
3
+ private client;
4
+ private bucket;
5
+ private prefix;
6
+ constructor(bucket: string, prefix: string, region?: string);
7
+ private key;
8
+ upload(sessionId: string, tarPath: string): Promise<boolean>;
9
+ download(sessionId: string, destPath: string): Promise<boolean>;
10
+ exists(sessionId: string): Promise<boolean>;
11
+ delete(sessionId: string): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=snapshot-s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-s3.d.ts","sourceRoot":"","sources":["../src/snapshot-s3.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,qBAAa,eAAgB,YAAW,aAAa;IACnD,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAM3D,OAAO,CAAC,GAAG;IAIL,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgB5D,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB/D,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY3C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/C"}
@@ -0,0 +1,75 @@
1
+ import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
2
+ import { createReadStream, createWriteStream } from 'node:fs';
3
+ import { pipeline } from 'node:stream/promises';
4
+ export class S3SnapshotStore {
5
+ client;
6
+ bucket;
7
+ prefix;
8
+ constructor(bucket, prefix, region) {
9
+ this.bucket = bucket;
10
+ this.prefix = prefix;
11
+ this.client = new S3Client({ region: region || process.env.ASH_S3_REGION || 'us-east-1' });
12
+ }
13
+ key(sessionId) {
14
+ return `${this.prefix}${sessionId}/workspace.tar.gz`;
15
+ }
16
+ async upload(sessionId, tarPath) {
17
+ try {
18
+ const stream = createReadStream(tarPath);
19
+ await this.client.send(new PutObjectCommand({
20
+ Bucket: this.bucket,
21
+ Key: this.key(sessionId),
22
+ Body: stream,
23
+ ContentType: 'application/gzip',
24
+ }));
25
+ return true;
26
+ }
27
+ catch (err) {
28
+ console.error(`[snapshot-s3] Upload failed for ${sessionId}:`, err);
29
+ return false;
30
+ }
31
+ }
32
+ async download(sessionId, destPath) {
33
+ try {
34
+ const resp = await this.client.send(new GetObjectCommand({
35
+ Bucket: this.bucket,
36
+ Key: this.key(sessionId),
37
+ }));
38
+ if (!resp.Body)
39
+ return false;
40
+ const ws = createWriteStream(destPath);
41
+ await pipeline(resp.Body, ws);
42
+ return true;
43
+ }
44
+ catch (err) {
45
+ if (err instanceof Error && err.name === 'NoSuchKey')
46
+ return false;
47
+ console.error(`[snapshot-s3] Download failed for ${sessionId}:`, err);
48
+ return false;
49
+ }
50
+ }
51
+ async exists(sessionId) {
52
+ try {
53
+ await this.client.send(new HeadObjectCommand({
54
+ Bucket: this.bucket,
55
+ Key: this.key(sessionId),
56
+ }));
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ async delete(sessionId) {
64
+ try {
65
+ await this.client.send(new DeleteObjectCommand({
66
+ Bucket: this.bucket,
67
+ Key: this.key(sessionId),
68
+ }));
69
+ }
70
+ catch (err) {
71
+ console.error(`[snapshot-s3] Delete failed for ${sessionId}:`, err);
72
+ }
73
+ }
74
+ }
75
+ //# sourceMappingURL=snapshot-s3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-s3.js","sourceRoot":"","sources":["../src/snapshot-s3.ts"],"names":[],"mappings":"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"}