@declaw/sdk 1.0.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ All notable changes to the Declaw TypeScript / JavaScript SDK are documented in
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.3]
9
+
10
+ ### Added
11
+
12
+ - `sandbox.pty.connect(pid, { onData })` — reattach a fresh data callback
13
+ to an already-running PTY session by pid. Multiple clients can
14
+ subscribe concurrently; each receives output from the moment it
15
+ connects (no scrollback replay).
16
+ - Server-side PTY session TTL. The `timeout` option on
17
+ `sandbox.pty.create()` is now enforced inside the sandbox — the PTY
18
+ is terminated when the deadline elapses, even if no client is
19
+ attached. `timeout: 0` keeps it alive indefinitely.
20
+
21
+ ### Changed
22
+
23
+ - `PtyHandle.wait()` now resolves with a `PtyResult` object (`{ exitCode }`)
24
+ instead of a bare number. This matches the shape of other lifecycle
25
+ APIs and leaves room for future fields (e.g. signal). Existing
26
+ numeric access becomes `(await handle.wait()).exitCode`.
27
+
28
+ ## [1.0.2]
29
+
30
+ ### Added
31
+
32
+ - Real interactive PTY support. `sandbox.pty.create()` now returns a
33
+ `PtyHandle` with both an `onData` callback and an `async *stream()`
34
+ iterator that deliver raw terminal bytes as they arrive from the
35
+ sandbox (ANSI escapes included).
36
+ - `PtyHandle.sendInput`, `PtyHandle.resize`, `PtyHandle.kill`,
37
+ `PtyHandle.disconnect`, `PtyHandle.wait()` for full lifecycle control.
38
+ - `ApiClient.streamGet()` for SSE consumers.
39
+ - `PtyHandle` exported from the package root.
40
+
41
+ ### Changed
42
+
43
+ - `Pty.create()` accepts `onData` in `PtyCreateOpts` and returns the new
44
+ `PtyHandle` type. Existing fields are unchanged; callers that only
45
+ used the returned handle for `sendInput`/`resize`/`kill` continue to
46
+ work without edits.
47
+
8
48
  ## [1.0.0]
9
49
 
10
50
  First stable release. The public API described below is covered by
package/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  NotFoundError: () => NotFoundError,
43
43
  PIIType: () => PIIType,
44
44
  Pty: () => Pty,
45
+ PtyHandle: () => PtyHandle,
45
46
  RedactionAction: () => RedactionAction,
46
47
  Sandbox: () => Sandbox,
47
48
  SandboxError: () => SandboxError,
@@ -263,6 +264,13 @@ var ApiClient = class {
263
264
  async stream(path, opts) {
264
265
  return this.requestWithRetry("POST", path, opts, true);
265
266
  }
267
+ /**
268
+ * Send a GET request and return the raw Response for SSE streaming.
269
+ * Does NOT parse the response body. Used by PTY stream consumers.
270
+ */
271
+ async streamGet(path, opts) {
272
+ return this.requestWithRetry("GET", path, opts, true);
273
+ }
266
274
  /**
267
275
  * Abort all in-flight requests and release resources.
268
276
  */
@@ -542,18 +550,12 @@ function parseInjectionDefenseConfig(data) {
542
550
  // src/security/audit.ts
543
551
  function createAuditConfig(opts) {
544
552
  return {
545
- enabled: opts?.enabled ?? false,
546
- logRequestBody: opts?.logRequestBody ?? true,
547
- logResponseBody: opts?.logResponseBody ?? false,
548
- retentionHours: opts?.retentionHours ?? 24
553
+ enabled: opts?.enabled ?? true
549
554
  };
550
555
  }
551
556
  function parseAuditConfig(data) {
552
557
  return {
553
- enabled: data.enabled ?? false,
554
- logRequestBody: data.log_request_body ?? data.logRequestBody ?? true,
555
- logResponseBody: data.log_response_body ?? data.logResponseBody ?? false,
556
- retentionHours: data.retention_hours ?? data.retentionHours ?? 24
558
+ enabled: data.enabled ?? true
557
559
  };
558
560
  }
559
561
  function parseAuditEntry(data) {
@@ -850,7 +852,7 @@ function createSecurityPolicy(opts) {
850
852
  injectionDefense: opts?.injectionDefense ?? false,
851
853
  transformations: opts?.transformations ?? [],
852
854
  network: opts?.network,
853
- audit: opts?.audit ?? false,
855
+ audit: opts?.audit ?? true,
854
856
  envSecurity: opts?.envSecurity ?? createEnvSecurityConfig(),
855
857
  toxicity: opts?.toxicity,
856
858
  codeSecurity: opts?.codeSecurity,
@@ -865,7 +867,7 @@ function parseSecurityPolicy(data) {
865
867
  injectionDefense: typeof injDef === "boolean" ? injDef : injDef ? parseInjectionDefenseConfig(injDef) : false,
866
868
  transformations: Array.isArray(data.transformations) ? data.transformations.map((t) => parseTransformationRule(t)) : [],
867
869
  network: data.network ? parseNetworkPolicy(data.network) : void 0,
868
- audit: typeof auditData === "boolean" ? auditData : auditData ? parseAuditConfig(auditData) : false,
870
+ audit: typeof auditData === "boolean" ? auditData : auditData ? parseAuditConfig(auditData) : true,
869
871
  envSecurity: data.env_security ?? data.envSecurity ? parseEnvSecurityConfig(data.env_security ?? data.envSecurity) : createEnvSecurityConfig(),
870
872
  toxicity: data.toxicity ? parseToxicityConfig(data.toxicity) : void 0,
871
873
  codeSecurity: data.code_security ?? data.codeSecurity ? parseCodeSecurityConfig(data.code_security ?? data.codeSecurity) : void 0,
@@ -894,10 +896,7 @@ function securityPolicyToJSON(policy) {
894
896
  }
895
897
  const auditConfig = typeof policy.audit === "boolean" ? createAuditConfig({ enabled: policy.audit }) : policy.audit;
896
898
  const audit = {
897
- enabled: auditConfig.enabled,
898
- log_request_body: auditConfig.logRequestBody,
899
- log_response_body: auditConfig.logResponseBody,
900
- retention_hours: auditConfig.retentionHours
899
+ enabled: auditConfig.enabled
901
900
  };
902
901
  const envSec = {
903
902
  mask_patterns: policy.envSecurity.maskPatterns,
@@ -1364,6 +1363,11 @@ var Filesystem = class {
1364
1363
  /**
1365
1364
  * Write data to a file.
1366
1365
  *
1366
+ * When `data` is a `Uint8Array`, the SDK streams the raw bytes to the
1367
+ * binary-safe `PUT /files/raw` endpoint (500 MiB cap). When it's a string,
1368
+ * the SDK uses the JSON `POST /files` endpoint. Callers do not need to
1369
+ * pick the transport.
1370
+ *
1367
1371
  * @param path - Absolute path to the file.
1368
1372
  * @param data - String or Uint8Array content to write.
1369
1373
  * @param opts - Optional user and request timeout.
@@ -1371,11 +1375,22 @@ var Filesystem = class {
1371
1375
  */
1372
1376
  async write(path, data, opts) {
1373
1377
  const user = opts?.user ?? DEFAULT_USER;
1374
- const strData = data instanceof Uint8Array ? new TextDecoder().decode(data) : data;
1378
+ if (data instanceof Uint8Array) {
1379
+ const result2 = await this.client.put(
1380
+ `/sandboxes/${this.sandboxId}/files/raw`,
1381
+ {
1382
+ params: { path, username: user },
1383
+ body: data,
1384
+ headers: { "Content-Type": "application/octet-stream" },
1385
+ timeout: opts?.requestTimeout
1386
+ }
1387
+ );
1388
+ return parseWriteInfo(result2);
1389
+ }
1375
1390
  const result = await this.client.post(
1376
1391
  `/sandboxes/${this.sandboxId}/files`,
1377
1392
  {
1378
- json: { path, data: strData, username: user },
1393
+ json: { path, data, username: user },
1379
1394
  timeout: opts?.requestTimeout
1380
1395
  }
1381
1396
  );
@@ -1384,24 +1399,58 @@ var Filesystem = class {
1384
1399
  /**
1385
1400
  * Write multiple files in a single batch request.
1386
1401
  *
1402
+ * The batch endpoint is JSON-only and cannot carry binary. Entries are
1403
+ * partitioned: string entries go through `POST /files/batch` in one call,
1404
+ * `Uint8Array` entries are streamed individually to `PUT /files/raw`.
1405
+ * Results are merged back in input order.
1406
+ *
1387
1407
  * @param files - Array of files to write.
1388
1408
  * @param opts - Optional user and request timeout.
1389
- * @returns Array of write info for each file.
1409
+ * @returns Array of write info for each file, in input order.
1390
1410
  */
1391
1411
  async writeFiles(files, opts) {
1392
1412
  const user = opts?.user ?? DEFAULT_USER;
1393
- const encodedFiles = files.map((f) => ({
1394
- path: f.path,
1395
- data: f.data instanceof Uint8Array ? new TextDecoder().decode(f.data) : f.data
1396
- }));
1397
- const result = await this.client.post(
1398
- `/sandboxes/${this.sandboxId}/files/batch`,
1399
- {
1400
- json: { files: encodedFiles, username: user },
1401
- timeout: opts?.requestTimeout
1413
+ const results = new Array(files.length);
1414
+ const strIndices = [];
1415
+ const bytesIndices = [];
1416
+ for (let i = 0; i < files.length; i++) {
1417
+ if (files[i].data instanceof Uint8Array) {
1418
+ bytesIndices.push(i);
1419
+ } else {
1420
+ strIndices.push(i);
1402
1421
  }
1403
- );
1404
- return (result ?? []).map(parseWriteInfo);
1422
+ }
1423
+ if (strIndices.length > 0) {
1424
+ const batchFiles = strIndices.map((i) => ({
1425
+ path: files[i].path,
1426
+ data: files[i].data
1427
+ }));
1428
+ const result = await this.client.post(
1429
+ `/sandboxes/${this.sandboxId}/files/batch`,
1430
+ {
1431
+ json: { files: batchFiles, username: user },
1432
+ timeout: opts?.requestTimeout
1433
+ }
1434
+ );
1435
+ const parsed = (result ?? []).map(parseWriteInfo);
1436
+ for (let k = 0; k < strIndices.length; k++) {
1437
+ results[strIndices[k]] = parsed[k];
1438
+ }
1439
+ }
1440
+ for (const i of bytesIndices) {
1441
+ const entry = files[i];
1442
+ const result = await this.client.put(
1443
+ `/sandboxes/${this.sandboxId}/files/raw`,
1444
+ {
1445
+ params: { path: entry.path, username: user },
1446
+ body: entry.data,
1447
+ headers: { "Content-Type": "application/octet-stream" },
1448
+ timeout: opts?.requestTimeout
1449
+ }
1450
+ );
1451
+ results[i] = parseWriteInfo(result);
1452
+ }
1453
+ return results.filter((r) => r !== void 0);
1405
1454
  }
1406
1455
  /**
1407
1456
  * List entries in a directory.
@@ -1540,52 +1589,188 @@ var Filesystem = class {
1540
1589
  };
1541
1590
 
1542
1591
  // src/sandbox/pty/pty.ts
1543
- var Pty = class {
1592
+ var PtyHandle = class {
1593
+ pid;
1544
1594
  sandboxId;
1545
1595
  client;
1546
- constructor(sandboxId, client) {
1596
+ exitPromise;
1597
+ resolveExit;
1598
+ aborter = new AbortController();
1599
+ constructor(pid, sandboxId, client, onData) {
1600
+ this.pid = pid;
1547
1601
  this.sandboxId = sandboxId;
1548
1602
  this.client = client;
1603
+ this.exitPromise = new Promise((resolve) => {
1604
+ this.resolveExit = resolve;
1605
+ });
1606
+ if (onData) {
1607
+ void this.consumeStream(onData);
1608
+ }
1609
+ }
1610
+ /** Forward keystrokes to the PTY. */
1611
+ async sendInput(data, requestTimeout) {
1612
+ const strData = typeof data === "string" ? data : new TextDecoder().decode(data);
1613
+ await this.client.post(
1614
+ `/sandboxes/${this.sandboxId}/pty/${this.pid}/stdin`,
1615
+ { json: { data: strData }, timeout: requestTimeout }
1616
+ );
1617
+ }
1618
+ /** Update the terminal size (TIOCSWINSZ inside the VM). */
1619
+ async resize(size, requestTimeout) {
1620
+ await this.client.patch(
1621
+ `/sandboxes/${this.sandboxId}/pty/${this.pid}`,
1622
+ {
1623
+ json: { size: { cols: size.cols, rows: size.rows } },
1624
+ timeout: requestTimeout
1625
+ }
1626
+ );
1627
+ }
1628
+ /** SIGKILL the remote process and close any open streams. */
1629
+ async kill(requestTimeout) {
1630
+ this.aborter.abort();
1631
+ const data = await this.client.delete(
1632
+ `/sandboxes/${this.sandboxId}/pty/${this.pid}`,
1633
+ { timeout: requestTimeout }
1634
+ );
1635
+ return data.killed === true;
1636
+ }
1637
+ /**
1638
+ * Stop consuming output without killing the process. The PTY keeps
1639
+ * running server-side and a fresh `stream()` call reattaches.
1640
+ */
1641
+ disconnect() {
1642
+ this.aborter.abort();
1643
+ }
1644
+ /** Resolves with the remote exit result when the PTY process exits. */
1645
+ wait() {
1646
+ return this.exitPromise;
1549
1647
  }
1550
1648
  /**
1551
- * Create a new PTY session.
1649
+ * Async iterator over raw output chunks. Use when you want to drive
1650
+ * the stream yourself:
1651
+ *
1652
+ * for await (const chunk of handle.stream()) { ... }
1552
1653
  *
1553
- * Sends POST /sandboxes/:id/pty.
1554
- * @returns A CommandHandle for the PTY process.
1654
+ * Don't mix this with `onData` on the same handle — they both try to
1655
+ * consume the same underlying SSE connection.
1555
1656
  */
1657
+ async *stream() {
1658
+ const response = await this.client.streamGet(
1659
+ `/sandboxes/${this.sandboxId}/pty/${this.pid}/stream`,
1660
+ { timeout: void 0 }
1661
+ );
1662
+ if (!response.body) {
1663
+ return;
1664
+ }
1665
+ const reader = response.body.getReader();
1666
+ const decoder = new TextDecoder();
1667
+ let buffer = "";
1668
+ try {
1669
+ while (true) {
1670
+ if (this.aborter.signal.aborted) return;
1671
+ const { value, done } = await reader.read();
1672
+ if (done) break;
1673
+ buffer += decoder.decode(value, { stream: true });
1674
+ let idx;
1675
+ while ((idx = buffer.indexOf("\n\n")) >= 0) {
1676
+ const frame = buffer.slice(0, idx);
1677
+ buffer = buffer.slice(idx + 2);
1678
+ const parsed = parseSSEFrame(frame);
1679
+ if (!parsed) continue;
1680
+ if (parsed.event === "exit") {
1681
+ this.resolveExit({ exitCode: parsed.exitCode ?? -1 });
1682
+ return;
1683
+ }
1684
+ if (parsed.bytes) {
1685
+ yield parsed.bytes;
1686
+ }
1687
+ }
1688
+ }
1689
+ } finally {
1690
+ reader.releaseLock();
1691
+ }
1692
+ }
1693
+ async consumeStream(onData) {
1694
+ try {
1695
+ for await (const chunk of this.stream()) {
1696
+ onData(chunk);
1697
+ }
1698
+ } catch (err) {
1699
+ if (!this.aborter.signal.aborted) {
1700
+ throw err;
1701
+ }
1702
+ } finally {
1703
+ this.resolveExit({ exitCode: -1 });
1704
+ }
1705
+ }
1706
+ };
1707
+ function parseSSEFrame(frame) {
1708
+ let event = "message";
1709
+ let data = "";
1710
+ for (const line of frame.split("\n")) {
1711
+ if (line.startsWith("event:")) event = line.slice(6).trim();
1712
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
1713
+ }
1714
+ if (!event) return null;
1715
+ if (event === "exit") {
1716
+ try {
1717
+ const parsed = JSON.parse(data);
1718
+ return { event, exitCode: parsed.exit_code ?? -1 };
1719
+ } catch {
1720
+ return { event, exitCode: -1 };
1721
+ }
1722
+ }
1723
+ if (event === "data") {
1724
+ try {
1725
+ const parsed = JSON.parse(data);
1726
+ if (typeof parsed.data === "string") {
1727
+ const bin = atob(parsed.data);
1728
+ const bytes = new Uint8Array(bin.length);
1729
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1730
+ return { event, bytes };
1731
+ }
1732
+ } catch {
1733
+ return null;
1734
+ }
1735
+ }
1736
+ return null;
1737
+ }
1738
+ var Pty = class {
1739
+ sandboxId;
1740
+ client;
1741
+ constructor(sandboxId, client) {
1742
+ this.sandboxId = sandboxId;
1743
+ this.client = client;
1744
+ }
1556
1745
  async create(opts) {
1557
1746
  const size = opts?.size ?? { cols: 80, rows: 24 };
1558
1747
  const user = opts?.user ?? "user";
1559
1748
  const body = {
1560
1749
  size: { cols: size.cols, rows: size.rows },
1561
- user
1750
+ user,
1751
+ timeout: opts?.timeout ?? 3600
1562
1752
  };
1563
- if (opts?.cwd !== void 0) {
1564
- body.cwd = opts.cwd;
1565
- }
1566
- if (opts?.envs !== void 0) {
1567
- body.envs = opts.envs;
1568
- }
1569
- if (opts?.timeout !== void 0) {
1570
- body.timeout = opts.timeout;
1571
- }
1753
+ if (opts?.cwd !== void 0) body.cwd = opts.cwd;
1754
+ if (opts?.envs !== void 0) body.envs = opts.envs;
1572
1755
  const data = await this.client.post(
1573
1756
  `/sandboxes/${this.sandboxId}/pty`,
1574
- {
1575
- json: body,
1576
- timeout: opts?.requestTimeout
1577
- }
1757
+ { json: body, timeout: opts?.requestTimeout }
1578
1758
  );
1579
- const response = data;
1580
- const pid = response.pid;
1581
- return new CommandHandle(pid, this.sandboxId, this.client);
1759
+ const pid = data.pid;
1760
+ return new PtyHandle(pid, this.sandboxId, this.client, opts?.onData);
1582
1761
  }
1583
1762
  /**
1584
- * Kill a PTY session.
1763
+ * Reattach to an already-running PTY by its pid.
1585
1764
  *
1586
- * Sends DELETE /sandboxes/:id/pty/:pid.
1587
- * @returns true if the process was killed, false if already dead.
1765
+ * Returns a fresh `PtyHandle` that streams the live output of the
1766
+ * existing session. Multiple clients can subscribe to the same pid
1767
+ * concurrently — each receives output from the moment it connects
1768
+ * (no scrollback replay).
1588
1769
  */
1770
+ connect(pid, opts) {
1771
+ return new PtyHandle(pid, this.sandboxId, this.client, opts?.onData);
1772
+ }
1773
+ // --- Low-level API by pid (kept for callers that already hold one). ---
1589
1774
  async kill(pid, requestTimeout) {
1590
1775
  const data = await this.client.delete(
1591
1776
  `/sandboxes/${this.sandboxId}/pty/${pid}`,
@@ -1593,27 +1778,13 @@ var Pty = class {
1593
1778
  );
1594
1779
  return data.killed === true;
1595
1780
  }
1596
- /**
1597
- * Send input to a PTY session.
1598
- *
1599
- * Sends POST /sandboxes/:id/pty/:pid/stdin.
1600
- * If data is a Uint8Array, it is decoded to a string using TextDecoder.
1601
- */
1602
1781
  async sendStdin(pid, data, requestTimeout) {
1603
1782
  const strData = typeof data === "string" ? data : new TextDecoder().decode(data);
1604
1783
  await this.client.post(
1605
1784
  `/sandboxes/${this.sandboxId}/pty/${pid}/stdin`,
1606
- {
1607
- json: { data: strData },
1608
- timeout: requestTimeout
1609
- }
1785
+ { json: { data: strData }, timeout: requestTimeout }
1610
1786
  );
1611
1787
  }
1612
- /**
1613
- * Resize a PTY session.
1614
- *
1615
- * Sends PATCH /sandboxes/:id/pty/:pid.
1616
- */
1617
1788
  async resize(pid, size, requestTimeout) {
1618
1789
  await this.client.patch(
1619
1790
  `/sandboxes/${this.sandboxId}/pty/${pid}`,
@@ -2398,6 +2569,7 @@ var Template = class {
2398
2569
  NotFoundError,
2399
2570
  PIIType,
2400
2571
  Pty,
2572
+ PtyHandle,
2401
2573
  RedactionAction,
2402
2574
  Sandbox,
2403
2575
  SandboxError,