@hasna/sandboxes 0.1.29 → 0.1.30

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/index.js CHANGED
@@ -5949,6 +5949,17 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (6);
5949
5949
  `
5950
5950
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
5951
5951
  INSERT OR IGNORE INTO _migrations (id) VALUES (7);
5952
+ `,
5953
+ `
5954
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC);
5955
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC);
5956
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC);
5957
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC);
5958
+ CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC);
5959
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC);
5960
+ CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC);
5961
+ CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC);
5962
+ INSERT OR IGNORE INTO _migrations (id) VALUES (8);
5952
5963
  `
5953
5964
  ];
5954
5965
  var db = null;
@@ -5994,71 +6005,108 @@ function shortId() {
5994
6005
  function now() {
5995
6006
  return new Date().toISOString().replace("T", " ").replace("Z", "");
5996
6007
  }
6008
+ var PARTIAL_ID_TABLES = new Set([
6009
+ "agents",
6010
+ "projects",
6011
+ "sandbox_sessions",
6012
+ "sandboxes",
6013
+ "snapshots",
6014
+ "templates",
6015
+ "webhooks"
6016
+ ]);
5997
6017
  function resolvePartialId(table, partialId) {
6018
+ if (!PARTIAL_ID_TABLES.has(table)) {
6019
+ throw new Error(`Invalid partial-id table: ${table}`);
6020
+ }
5998
6021
  const database = getDatabase();
5999
- const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? || '%'`).all(partialId);
6000
- if (rows.length === 1)
6001
- return rows[0].id;
6002
- if (rows.length === 0)
6003
- return null;
6004
- const exact = rows.find((r) => r.id === partialId);
6022
+ const exact = database.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
6005
6023
  if (exact)
6006
6024
  return exact.id;
6025
+ const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? ORDER BY id LIMIT 2`).all(`${partialId}%`);
6026
+ if (rows.length === 1)
6027
+ return rows[0].id;
6007
6028
  return null;
6008
6029
  }
6009
- // src/db/cloud-config.ts
6030
+ // src/db/storage-config.ts
6010
6031
  import { existsSync as existsSync2, readFileSync } from "fs";
6011
6032
  import { homedir } from "os";
6012
6033
  import { join as join2 } from "path";
6013
- var CONFIG_PATH = join2(homedir(), ".hasna", "sandboxes", "cloud", "config.json");
6014
- function isMode(value) {
6015
- return value === "local" || value === "hybrid" || value === "cloud";
6034
+ var STORAGE_CONFIG_PATH = join2(homedir(), ".hasna", "sandboxes", "storage", "config.json");
6035
+ var SANDBOXES_STORAGE_ENV = "HASNA_SANDBOXES_DATABASE_URL";
6036
+ var SANDBOXES_STORAGE_FALLBACK_ENV = "SANDBOXES_DATABASE_URL";
6037
+ var SANDBOXES_STORAGE_MODE_ENV = "HASNA_SANDBOXES_STORAGE_MODE";
6038
+ var SANDBOXES_STORAGE_MODE_FALLBACK_ENV = "SANDBOXES_STORAGE_MODE";
6039
+ var STORAGE_DATABASE_ENV = [SANDBOXES_STORAGE_ENV, SANDBOXES_STORAGE_FALLBACK_ENV];
6040
+ var STORAGE_MODE_ENV = [SANDBOXES_STORAGE_MODE_ENV, SANDBOXES_STORAGE_MODE_FALLBACK_ENV];
6041
+ function readEnv(name) {
6042
+ const value = process.env[name]?.trim();
6043
+ return value || undefined;
6044
+ }
6045
+ function normalizeStorageMode(value) {
6046
+ const normalized = value?.trim().toLowerCase();
6047
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
6048
+ return normalized;
6049
+ return;
6050
+ }
6051
+ function getStorageDatabaseEnvName() {
6052
+ for (const name of STORAGE_DATABASE_ENV) {
6053
+ if (readEnv(name))
6054
+ return name;
6055
+ }
6056
+ return null;
6057
+ }
6058
+ function getStorageDatabaseEnv() {
6059
+ const name = getStorageDatabaseEnvName();
6060
+ return name ? { name } : null;
6016
6061
  }
6017
- function envConnectionString() {
6018
- return process.env["HASNA_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["OPEN_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["SANDBOXES_CLOUD_DATABASE_URL"];
6062
+ function getStorageDatabaseUrl() {
6063
+ const env = getStorageDatabaseEnv();
6064
+ return env ? readEnv(env.name) : undefined;
6019
6065
  }
6020
- function getCloudConfig() {
6066
+ function getStorageConfig() {
6021
6067
  const config = {
6022
6068
  mode: "local",
6023
6069
  rds: {
6024
6070
  host: "",
6025
6071
  port: 5432,
6026
6072
  username: "",
6027
- password_env: "SANDBOXES_CLOUD_DATABASE_PASSWORD",
6073
+ password_env: "SANDBOXES_DATABASE_PASSWORD",
6028
6074
  ssl: true
6029
6075
  }
6030
6076
  };
6031
- if (existsSync2(CONFIG_PATH)) {
6077
+ if (existsSync2(STORAGE_CONFIG_PATH)) {
6032
6078
  try {
6033
- const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
6034
- config.mode = raw.mode ?? config.mode;
6079
+ const raw = JSON.parse(readFileSync(STORAGE_CONFIG_PATH, "utf-8"));
6080
+ config.mode = normalizeStorageMode(raw.mode) ?? config.mode;
6035
6081
  config.rds = { ...config.rds, ...raw.rds ?? {} };
6036
6082
  } catch {}
6037
6083
  }
6038
- const modeOverride = process.env["HASNA_SANDBOXES_CLOUD_MODE"] ?? process.env["OPEN_SANDBOXES_CLOUD_MODE"] ?? process.env["SANDBOXES_CLOUD_MODE"];
6039
- if (isMode(modeOverride)) {
6040
- config.mode = modeOverride;
6041
- } else if (envConnectionString() && config.mode === "local") {
6084
+ const modeOverride = readEnv(SANDBOXES_STORAGE_MODE_ENV) ?? readEnv(SANDBOXES_STORAGE_MODE_FALLBACK_ENV);
6085
+ const mode = normalizeStorageMode(modeOverride);
6086
+ if (mode) {
6087
+ config.mode = mode;
6088
+ } else if (getStorageDatabaseUrl() && config.mode === "local") {
6042
6089
  config.mode = "hybrid";
6043
6090
  }
6044
6091
  return config;
6045
6092
  }
6046
- function getConnectionString(dbName = "sandboxes") {
6047
- const direct = envConnectionString();
6093
+ function getStorageConnectionString(dbName = "sandboxes") {
6094
+ const direct = getStorageDatabaseUrl();
6048
6095
  if (direct)
6049
6096
  return direct;
6050
- const config = getCloudConfig();
6097
+ const config = getStorageConfig();
6051
6098
  const { host, port, username, password_env, ssl } = config.rds;
6052
6099
  if (!host || !username) {
6053
- throw new Error("Cloud database is not configured. Set HASNA_SANDBOXES_CLOUD_DATABASE_URL or configure ~/.hasna/sandboxes/cloud/config.json.");
6100
+ throw new Error("Storage database is not configured. Set HASNA_SANDBOXES_DATABASE_URL or configure ~/.hasna/sandboxes/storage/config.json.");
6054
6101
  }
6055
6102
  const password = process.env[password_env];
6056
6103
  if (!password) {
6057
- throw new Error(`Cloud database password is not set. Export ${password_env}.`);
6104
+ throw new Error(`Storage database password is not set. Export ${password_env}.`);
6058
6105
  }
6059
6106
  const sslParam = ssl ? "?sslmode=require" : "";
6060
6107
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
6061
6108
  }
6109
+ var getConnectionString = getStorageConnectionString;
6062
6110
  // node_modules/pg/esm/index.mjs
6063
6111
  var import_lib = __toESM(require_lib2(), 1);
6064
6112
  var Client = import_lib.default.Client;
@@ -6148,6 +6196,9 @@ var PG_MIGRATIONS = [
6148
6196
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_status ON sandboxes(status)`,
6149
6197
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider ON sandboxes(provider)`,
6150
6198
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_project ON sandboxes(project_id)`,
6199
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC)`,
6200
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC)`,
6201
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC)`,
6151
6202
  `CREATE TABLE IF NOT EXISTS sandbox_sessions (
6152
6203
  id TEXT PRIMARY KEY,
6153
6204
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -6161,6 +6212,9 @@ var PG_MIGRATIONS = [
6161
6212
  )`,
6162
6213
  `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox ON sandbox_sessions(sandbox_id)`,
6163
6214
  `CREATE INDEX IF NOT EXISTS idx_sessions_status ON sandbox_sessions(status)`,
6215
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC)`,
6216
+ `CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC)`,
6217
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC)`,
6164
6218
  `CREATE TABLE IF NOT EXISTS sandbox_events (
6165
6219
  id TEXT PRIMARY KEY,
6166
6220
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -6172,6 +6226,8 @@ var PG_MIGRATIONS = [
6172
6226
  `CREATE INDEX IF NOT EXISTS idx_events_sandbox ON sandbox_events(sandbox_id)`,
6173
6227
  `CREATE INDEX IF NOT EXISTS idx_events_session ON sandbox_events(session_id)`,
6174
6228
  `CREATE INDEX IF NOT EXISTS idx_events_type ON sandbox_events(type)`,
6229
+ `CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC)`,
6230
+ `CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC)`,
6175
6231
  `CREATE TABLE IF NOT EXISTS webhooks (
6176
6232
  id TEXT PRIMARY KEY,
6177
6233
  url TEXT NOT NULL,
@@ -6261,8 +6317,8 @@ async function applyPgMigrations(connectionString) {
6261
6317
  }
6262
6318
  return result;
6263
6319
  }
6264
- // src/db/cloud-sync.ts
6265
- var CLOUD_TABLES = [
6320
+ // src/db/storage-sync.ts
6321
+ var STORAGE_TABLES = [
6266
6322
  "projects",
6267
6323
  "agents",
6268
6324
  "sandboxes",
@@ -6273,6 +6329,7 @@ var CLOUD_TABLES = [
6273
6329
  "snapshots",
6274
6330
  "feedback"
6275
6331
  ];
6332
+ var SANDBOXES_STORAGE_TABLES = STORAGE_TABLES;
6276
6333
  var TABLE_KEYS = {
6277
6334
  projects: ["id"],
6278
6335
  agents: ["id"],
@@ -6356,21 +6413,26 @@ function upsertSqlite(db2, table, rows) {
6356
6413
  }
6357
6414
  return written;
6358
6415
  }
6359
- async function getCloudPg() {
6360
- return new PgAdapterAsync(getConnectionString("sandboxes"));
6416
+ async function getStoragePg() {
6417
+ return new PgAdapterAsync(getStorageConnectionString("sandboxes"));
6361
6418
  }
6362
- async function runCloudMigrations(remote) {
6419
+ async function runStorageMigrations(remote) {
6363
6420
  for (const migration of PG_MIGRATIONS) {
6364
6421
  await remote.exec(migration);
6365
6422
  }
6366
6423
  }
6367
- function getCloudStatus(db2 = getDatabase()) {
6368
- const config = getCloudConfig();
6424
+ function getStorageStatus(db2 = getDatabase()) {
6425
+ const config = getStorageConfig();
6426
+ const activeEnv = getStorageDatabaseEnv();
6369
6427
  return {
6428
+ configured: Boolean(activeEnv),
6370
6429
  mode: config.mode,
6371
- enabled: config.mode === "hybrid" || config.mode === "cloud",
6430
+ enabled: config.mode === "hybrid" || config.mode === "remote",
6431
+ env: STORAGE_DATABASE_ENV,
6432
+ activeEnv: activeEnv?.name ?? null,
6433
+ service: "sandboxes",
6372
6434
  db_path: getDbPath(),
6373
- tables: CLOUD_TABLES.map((table) => {
6435
+ tables: STORAGE_TABLES.map((table) => {
6374
6436
  try {
6375
6437
  const row = db2.query(`SELECT COUNT(*) as count FROM ${quoteId(table)}`).get();
6376
6438
  return { table, rows: row.count };
@@ -6380,12 +6442,12 @@ function getCloudStatus(db2 = getDatabase()) {
6380
6442
  })
6381
6443
  };
6382
6444
  }
6383
- async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
6445
+ async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
6384
6446
  const db2 = getDatabase();
6385
- const remote = await getCloudPg();
6447
+ const remote = await getStoragePg();
6386
6448
  const results = [];
6387
6449
  try {
6388
- await runCloudMigrations(remote);
6450
+ await runStorageMigrations(remote);
6389
6451
  for (const table of tables) {
6390
6452
  const result = { table, direction: "push", rowsRead: 0, rowsWritten: 0, errors: [] };
6391
6453
  try {
@@ -6402,12 +6464,12 @@ async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
6402
6464
  }
6403
6465
  return results;
6404
6466
  }
6405
- async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
6467
+ async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
6406
6468
  const db2 = getDatabase();
6407
- const remote = await getCloudPg();
6469
+ const remote = await getStoragePg();
6408
6470
  const results = [];
6409
6471
  try {
6410
- await runCloudMigrations(remote);
6472
+ await runStorageMigrations(remote);
6411
6473
  for (const table of tables) {
6412
6474
  const result = { table, direction: "pull", rowsRead: 0, rowsWritten: 0, errors: [] };
6413
6475
  try {
@@ -6424,17 +6486,23 @@ async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
6424
6486
  }
6425
6487
  return results;
6426
6488
  }
6427
- async function syncCloudChanges(tables = [...CLOUD_TABLES]) {
6489
+ async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
6428
6490
  return {
6429
- push: await pushCloudChanges(tables),
6430
- pull: await pullCloudChanges(tables)
6491
+ push: await pushStorageChanges(tables),
6492
+ pull: await pullStorageChanges(tables)
6431
6493
  };
6432
6494
  }
6433
- function parseCloudTables(raw) {
6495
+ function parseStorageTables(raw) {
6434
6496
  if (!raw)
6435
- return [...CLOUD_TABLES];
6497
+ return [...STORAGE_TABLES];
6436
6498
  const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
6437
- return requested.length > 0 ? requested : [...CLOUD_TABLES];
6499
+ if (requested.length === 0)
6500
+ return [...STORAGE_TABLES];
6501
+ const allowed = new Set(STORAGE_TABLES);
6502
+ const invalid = requested.filter((table) => !allowed.has(table));
6503
+ if (invalid.length > 0)
6504
+ throw new Error(`Unknown sandboxes sync table(s): ${invalid.join(", ")}`);
6505
+ return requested;
6438
6506
  }
6439
6507
  // src/db/sandboxes.ts
6440
6508
  init_types();
@@ -6674,7 +6742,7 @@ function listEvents(opts) {
6674
6742
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
6675
6743
  const limit = opts?.limit ?? 100;
6676
6744
  const offset = opts?.offset ?? 0;
6677
- const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
6745
+ const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC, rowid ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
6678
6746
  return rows.map(rowToEvent);
6679
6747
  }
6680
6748
  // src/db/agents.ts
@@ -7609,18 +7677,18 @@ export {
7609
7677
  uuid,
7610
7678
  updateSession,
7611
7679
  updateSandbox,
7612
- syncCloudChanges,
7680
+ syncStorageChanges,
7613
7681
  shortId,
7614
7682
  setConfigValue,
7615
7683
  saveConfig,
7616
- runCloudMigrations,
7684
+ runStorageMigrations,
7617
7685
  resolvePartialId,
7618
7686
  resolveImage,
7619
7687
  resetDatabase,
7620
7688
  registerAgent,
7621
- pushCloudChanges,
7622
- pullCloudChanges,
7623
- parseCloudTables,
7689
+ pushStorageChanges,
7690
+ pullStorageChanges,
7691
+ parseStorageTables,
7624
7692
  now,
7625
7693
  loadConfig,
7626
7694
  listWebhooks,
@@ -7635,6 +7703,13 @@ export {
7635
7703
  getWebhook,
7636
7704
  getTemplateByName,
7637
7705
  getTemplate,
7706
+ getStorageStatus,
7707
+ getStoragePg,
7708
+ getStorageDatabaseUrl,
7709
+ getStorageDatabaseEnvName,
7710
+ getStorageDatabaseEnv,
7711
+ getStorageConnectionString,
7712
+ getStorageConfig,
7638
7713
  getSnapshot,
7639
7714
  getSession,
7640
7715
  getSandbox,
@@ -7648,9 +7723,6 @@ export {
7648
7723
  getDatabase,
7649
7724
  getConnectionString,
7650
7725
  getConfigValue,
7651
- getCloudStatus,
7652
- getCloudPg,
7653
- getCloudConfig,
7654
7726
  getBuiltinImageSetupScript,
7655
7727
  getAgentDriver,
7656
7728
  getAgentByName,
@@ -7682,14 +7754,21 @@ export {
7682
7754
  SessionNotFoundError,
7683
7755
  SandboxesSDK,
7684
7756
  SandboxNotFoundError,
7757
+ STORAGE_TABLES,
7758
+ STORAGE_MODE_ENV,
7759
+ STORAGE_DATABASE_ENV,
7685
7760
  SESSION_STATUSES,
7686
7761
  SANDBOX_STATUSES,
7687
7762
  SANDBOX_PROVIDERS,
7763
+ SANDBOXES_STORAGE_TABLES,
7764
+ SANDBOXES_STORAGE_MODE_FALLBACK_ENV,
7765
+ SANDBOXES_STORAGE_MODE_ENV,
7766
+ SANDBOXES_STORAGE_FALLBACK_ENV,
7767
+ SANDBOXES_STORAGE_ENV,
7688
7768
  ProviderError,
7689
7769
  ProjectNotFoundError,
7690
7770
  PgAdapterAsync,
7691
7771
  EVENT_TYPES,
7692
- CLOUD_TABLES,
7693
7772
  BUILTIN_IMAGES,
7694
7773
  AgentNotFoundError,
7695
7774
  AGENT_TYPES