@tangle-network/tcloud 0.4.2 → 0.4.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/dist/cli.cjs CHANGED
@@ -38,6 +38,8 @@ var PrivateRouter = class {
38
38
  usage = /* @__PURE__ */ new Map();
39
39
  currentIndex = 0;
40
40
  totalRequests = 0;
41
+ /** Slug of the most-recently-selected operator. Populated on each selectOperator() hit. */
42
+ _lastSelectedSlug = null;
41
43
  constructor(config = {}) {
42
44
  this.config = {
43
45
  strategy: config.strategy || "round-robin",
@@ -183,6 +185,11 @@ var PrivateRouter = class {
183
185
  requestCount: (existing?.requestCount || 0) + 1,
184
186
  lastUsedAt: Date.now()
185
187
  });
188
+ this._lastSelectedSlug = op.slug;
189
+ }
190
+ /** Slug of the most-recently-selected operator (null before the first call). */
191
+ get lastSelectedSlug() {
192
+ return this._lastSelectedSlug;
186
193
  }
187
194
  getLastUsedOperator() {
188
195
  let latest = null;
@@ -240,6 +247,7 @@ var PrivateRouter = class {
240
247
  };
241
248
 
242
249
  // src/client.ts
250
+ var ROTATING_MARKER = "__tcloudRotating";
243
251
  var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
244
252
  var SDK_VERSION = "0.4.0";
245
253
  async function proxiedFetch(privacy, url, init, streaming) {
@@ -337,6 +345,54 @@ var TCloudClient = class _TCloudClient {
337
345
  const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
338
346
  return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
339
347
  }
348
+ /**
349
+ * Build a client that rotates which operator serves each call. Mirrors
350
+ * {@link TCloudClient.shielded} in shape: returns a standard `TCloudClient`
351
+ * that behaves identically for the OpenAI-compatible surface but
352
+ * dispatches each chat/completions/embeddings request through a
353
+ * {@link PrivateRouter} — different operator per call per the chosen
354
+ * strategy.
355
+ *
356
+ * ```ts
357
+ * const tcloud = TCloudClient.rotating({
358
+ * apiKey: process.env.TCLOUD_API_KEY,
359
+ * routing: { strategy: 'min-exposure' },
360
+ * })
361
+ * await tcloud.ask('hello')
362
+ * tcloud.getRotationStats() // { callsByOperator: { ... }, currentOperator: '…' }
363
+ * ```
364
+ *
365
+ * Rotation is meaningful only for stateless calls. Sandbox-harness
366
+ * sessions bind to a single operator for the lifetime of the session;
367
+ * `rotating()` clients refuse to dispatch them — see {@link bridge}.
368
+ */
369
+ static rotating(config = {}) {
370
+ const routing = config.routing ?? {};
371
+ const strategy = routing.strategy ?? "min-exposure";
372
+ const { routing: _omit, ...base } = config;
373
+ const client = new _TCloudClient(base);
374
+ const router = new PrivateRouter({
375
+ strategy,
376
+ minOperators: routing.minOperators ?? 1,
377
+ maxRequestsPerOperator: routing.maxRequestsPerOperator,
378
+ excludeOperators: routing.excludeOperators,
379
+ preferRegions: routing.preferRegions
380
+ });
381
+ if (routing.pool && routing.pool.length > 0) {
382
+ router.setOperators(routing.pool);
383
+ client._cachedOperators = routing.pool;
384
+ client._operatorsCachedAt = Date.now();
385
+ }
386
+ ;
387
+ client.privateRouter = router;
388
+ Object.defineProperty(client, ROTATING_MARKER, {
389
+ value: true,
390
+ enumerable: false,
391
+ configurable: false,
392
+ writable: false
393
+ });
394
+ return client;
395
+ }
340
396
  constructor(config = {}) {
341
397
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
342
398
  this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
@@ -652,10 +708,38 @@ var TCloudClient = class _TCloudClient {
652
708
  *
653
709
  * Sessions persist across process restarts — use the same `resume` id
654
710
  * to land on the same CLI conversation (context intact, no replay tax).
711
+ *
712
+ * Guard: clients built via {@link TCloudClient.rotating} cannot dispatch
713
+ * sandbox-harness sessions (rotation rotates per call; a sandbox session
714
+ * binds to one operator). Attempting `bridge({ harness: 'sandbox' })` on
715
+ * a rotating client throws.
655
716
  */
656
717
  bridge(cfg) {
718
+ if (cfg.harness === "sandbox" && this[ROTATING_MARKER] === true) {
719
+ throw new Error(
720
+ "TCloudClient.rotating() cannot dispatch sandbox-harness sessions.\nSandbox sessions bind to a single operator; rotation is meaningful only for\nstateless calls. Use TCloudClient.shielded() + AgentProfile.confidential.tee\nfor privacy-preserving sandbox execution instead."
721
+ );
722
+ }
657
723
  return new BridgeSession(this, cfg);
658
724
  }
725
+ /**
726
+ * Rotation stats — populated only on clients created via
727
+ * {@link TCloudClient.rotating}. Non-rotating clients return an empty
728
+ * counter and `currentOperator: null`.
729
+ */
730
+ getRotationStats() {
731
+ if (!this.privateRouter) {
732
+ return { callsByOperator: {}, currentOperator: null };
733
+ }
734
+ const callsByOperator = {};
735
+ for (const row of this.privateRouter.getStats().operatorBreakdown) {
736
+ callsByOperator[row.slug] = row.requests;
737
+ }
738
+ return {
739
+ callsByOperator,
740
+ currentOperator: this.privateRouter.lastSelectedSlug
741
+ };
742
+ }
659
743
  /** Convenience: send a single message and get the text response */
660
744
  async ask(message, modelOrOptions) {
661
745
  const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
@@ -1157,6 +1241,8 @@ var BridgeSession = class _BridgeSession {
1157
1241
  this.client = client;
1158
1242
  this.cfg = cfg;
1159
1243
  }
1244
+ client;
1245
+ cfg;
1160
1246
  /** Full chat completion (non-streaming). */
1161
1247
  async chat(options) {
1162
1248
  return this.client.chat({ ...options, bridge: this.cfg });
@@ -1226,6 +1312,7 @@ var TCloudError = class extends Error {
1226
1312
  this.status = status;
1227
1313
  this.name = "TCloudError";
1228
1314
  }
1315
+ status;
1229
1316
  };
1230
1317
 
1231
1318
  // src/shielded.ts
@@ -1481,7 +1568,208 @@ async function replenishDirect(fundingKey, tokenAddress, amount, commitment, spe
1481
1568
  }
1482
1569
  }
1483
1570
 
1571
+ // src/sandbox.ts
1572
+ var import_sandbox = require("@tangle-network/sandbox");
1573
+ var import_node_crypto = require("crypto");
1574
+ var import_tcloud_attestation = require("@tangle-network/tcloud-attestation");
1575
+ var DEFAULT_SANDBOX_URL = "https://sandbox.tangle.tools";
1576
+ var TCloudSandbox = class {
1577
+ client;
1578
+ apiKey;
1579
+ baseUrl;
1580
+ timeoutMs;
1581
+ constructor(config) {
1582
+ this.apiKey = config.apiKey;
1583
+ this.baseUrl = (config.baseUrl ?? DEFAULT_SANDBOX_URL).replace(/\/+$/, "");
1584
+ this.timeoutMs = config.timeoutMs;
1585
+ this.client = new import_sandbox.Sandbox({
1586
+ apiKey: this.apiKey,
1587
+ baseUrl: this.baseUrl,
1588
+ timeoutMs: config.timeoutMs
1589
+ });
1590
+ }
1591
+ async create(options) {
1592
+ const effectiveVerify = shouldVerifyAttestation(options);
1593
+ const attestationPolicy = buildAttestationPolicy(options);
1594
+ const createOptions = buildSandboxCreateOptions({
1595
+ ...options,
1596
+ verify: effectiveVerify
1597
+ });
1598
+ const sandbox2 = await this.client.create(createOptions);
1599
+ this.attachTeeApiFallbacks(sandbox2);
1600
+ let attestation = attestationFromMetadata(sandbox2.metadata);
1601
+ if ((effectiveVerify || createOptions.confidential?.attestationNonce) && !attestation) {
1602
+ const getTeeAttestation = sandbox2.getTeeAttestation;
1603
+ if (typeof getTeeAttestation !== "function") {
1604
+ throw new Error("Installed @tangle-network/sandbox does not expose TEE attestation fetching");
1605
+ }
1606
+ attestation = (await getTeeAttestation.call(
1607
+ sandbox2,
1608
+ createOptions.confidential?.attestationNonce ? { attestationNonce: createOptions.confidential.attestationNonce } : void 0
1609
+ )).attestation;
1610
+ }
1611
+ if (effectiveVerify && !attestation) {
1612
+ throw new Error("TEE attestation verification requested but no evidence was returned");
1613
+ }
1614
+ const verification = effectiveVerify ? await (0, import_tcloud_attestation.verifyAttestationAsync)(attestation, {
1615
+ ...attestationPolicy,
1616
+ expectedNonce: createOptions.confidential?.attestationNonce
1617
+ }) : void 0;
1618
+ if (verification && !verification.valid) {
1619
+ throw new Error(`TEE attestation verification failed: ${verification.errors.join("; ")}`);
1620
+ }
1621
+ return {
1622
+ sandbox: sandbox2,
1623
+ attestation,
1624
+ verification,
1625
+ attestationNonce: createOptions.confidential?.attestationNonce,
1626
+ attestationStatus: {
1627
+ requested: Boolean(options.tee),
1628
+ evidenceReturned: Boolean(attestation),
1629
+ verified: Boolean(verification?.valid),
1630
+ nonceBound: Boolean(createOptions.confidential?.attestationNonce && verification?.valid),
1631
+ errors: verification?.errors ?? []
1632
+ }
1633
+ };
1634
+ }
1635
+ attachTeeApiFallbacks(sandbox2) {
1636
+ if (typeof sandbox2 !== "object" || sandbox2 === null) return;
1637
+ const target = sandbox2;
1638
+ if (typeof target.id !== "string" || target.id.length === 0) return;
1639
+ if (typeof target.getTeeAttestation !== "function") {
1640
+ target.getTeeAttestation = (options) => this.fetchTeeAttestation(target.id, options?.attestationNonce);
1641
+ }
1642
+ if (typeof target.getTeePublicKey !== "function") {
1643
+ target.getTeePublicKey = () => this.fetchTeePublicKey(target.id);
1644
+ }
1645
+ }
1646
+ async fetchTeeAttestation(sandboxId, attestationNonce) {
1647
+ const response = await this.fetchSandboxApi(
1648
+ `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/attestation`,
1649
+ {
1650
+ method: attestationNonce ? "POST" : "GET",
1651
+ body: attestationNonce ? JSON.stringify({ attestation_nonce: attestationNonce }) : void 0
1652
+ }
1653
+ );
1654
+ const data = await response.json();
1655
+ if (attestationNonce && !data.attestationNonce) {
1656
+ data.attestationNonce = attestationNonce;
1657
+ }
1658
+ return data;
1659
+ }
1660
+ async fetchTeePublicKey(sandboxId) {
1661
+ const response = await this.fetchSandboxApi(
1662
+ `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/public-key`,
1663
+ { method: "GET" }
1664
+ );
1665
+ return response.json();
1666
+ }
1667
+ async fetchSandboxApi(path2, options) {
1668
+ const headers = new Headers(options.headers);
1669
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
1670
+ if (options.body && !headers.has("Content-Type")) {
1671
+ headers.set("Content-Type", "application/json");
1672
+ }
1673
+ const response = await fetch(`${this.baseUrl}${path2}`, {
1674
+ ...options,
1675
+ headers,
1676
+ signal: options.signal ?? (this.timeoutMs ? AbortSignal.timeout(this.timeoutMs) : void 0)
1677
+ });
1678
+ if (!response.ok) {
1679
+ const body = await response.text();
1680
+ throw new Error(`Sandbox API ${path2} failed with HTTP ${response.status}: ${body}`);
1681
+ }
1682
+ return response;
1683
+ }
1684
+ };
1685
+ function buildSandboxCreateOptions(options) {
1686
+ if (!options.tee && (options.sealed || options.attestationNonce || options.verify)) {
1687
+ throw new Error("TEE options require a tee value");
1688
+ }
1689
+ const shouldGenerateNonce = options.attestationNonce === "auto" || shouldVerifyAttestation(options) && !options.attestationNonce;
1690
+ const attestationNonce = shouldGenerateNonce ? generateAttestationNonce() : options.attestationNonce;
1691
+ if (attestationNonce) {
1692
+ validateAttestationNonce(attestationNonce);
1693
+ }
1694
+ return {
1695
+ name: options.name,
1696
+ environment: options.environment ?? options.image,
1697
+ sshEnabled: options.ssh || void 0,
1698
+ git: options.gitUrl ? {
1699
+ url: options.gitUrl,
1700
+ ref: options.gitRef
1701
+ } : void 0,
1702
+ resources: options.cpu || options.memoryMb || options.diskGb ? {
1703
+ cpuCores: options.cpu,
1704
+ memoryMB: options.memoryMb,
1705
+ diskGB: options.diskGb
1706
+ } : void 0,
1707
+ backend: options.backend ? { type: options.backend } : void 0,
1708
+ confidential: options.tee ? {
1709
+ tee: options.tee,
1710
+ sealed: options.sealed || void 0,
1711
+ attestationNonce,
1712
+ attestationRefresh: Boolean(attestationNonce)
1713
+ } : void 0
1714
+ };
1715
+ }
1716
+ function shouldVerifyAttestation(options) {
1717
+ return Boolean(options.tee || options.verify);
1718
+ }
1719
+ function buildAttestationPolicy(options) {
1720
+ if (!options.tee || options.tee === "any") return options.attestationPolicy ?? {};
1721
+ const requestedTypes = acceptedAttestationTypesForTee(options.tee);
1722
+ const acceptedTeeTypes = options.attestationPolicy?.acceptedTeeTypes;
1723
+ if (acceptedTeeTypes?.length && !acceptedTeeTypes.some((type) => requestedTypes.includes(type))) {
1724
+ throw new Error(
1725
+ `TEE attestation policy does not accept requested TEE type ${options.tee}`
1726
+ );
1727
+ }
1728
+ return {
1729
+ ...options.attestationPolicy,
1730
+ acceptedTeeTypes: acceptedTeeTypes?.length ? acceptedTeeTypes.filter((type) => requestedTypes.includes(type)) : requestedTypes
1731
+ };
1732
+ }
1733
+ function acceptedAttestationTypesForTee(tee) {
1734
+ switch (tee) {
1735
+ case "phala-dstack":
1736
+ return ["tdx", "phala-dstack"];
1737
+ case "gcp":
1738
+ return ["tdx", "sev-snp", "gcp"];
1739
+ case "azure":
1740
+ return ["sev-snp", "azure"];
1741
+ default:
1742
+ return [(0, import_tcloud_attestation.normalizeTeeType)(tee)];
1743
+ }
1744
+ }
1745
+ function validateAttestationNonce(value) {
1746
+ const normalized = value.trim().toLowerCase().replace(/^0x/, "");
1747
+ if (!/^[0-9a-f]+$/.test(normalized)) {
1748
+ throw new Error("attestation nonce must be hex");
1749
+ }
1750
+ if (normalized.length % 2 !== 0) {
1751
+ throw new Error("attestation nonce must have even hex length");
1752
+ }
1753
+ const bytes = normalized.length / 2;
1754
+ if (bytes < 32 || bytes > 64) {
1755
+ throw new Error(`attestation nonce must be 32-64 bytes, got ${bytes}`);
1756
+ }
1757
+ }
1758
+ function generateAttestationNonce(bytes = 32) {
1759
+ return Array.from((0, import_node_crypto.randomBytes)(bytes)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1760
+ }
1761
+ function attestationFromMetadata(metadata) {
1762
+ const raw = metadata?.teeAttestationJson;
1763
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
1764
+ try {
1765
+ return JSON.parse(raw);
1766
+ } catch {
1767
+ return void 0;
1768
+ }
1769
+ }
1770
+
1484
1771
  // src/index.ts
1772
+ var import_tcloud_attestation2 = require("@tangle-network/tcloud-attestation");
1485
1773
  var TCloud = class _TCloud extends TCloudClient {
1486
1774
  constructor(config) {
1487
1775
  super(config);
@@ -1510,6 +1798,21 @@ var TCloud = class _TCloud extends TCloudClient {
1510
1798
  static shielded(config) {
1511
1799
  return createShieldedClient(config);
1512
1800
  }
1801
+ /**
1802
+ * Create a client that rotates operators per call.
1803
+ * See {@link TCloudClient.rotating} for semantics.
1804
+ *
1805
+ * ```ts
1806
+ * const client = TCloud.rotating({
1807
+ * apiKey: process.env.TCLOUD_API_KEY,
1808
+ * routing: { strategy: 'min-exposure' },
1809
+ * })
1810
+ * const stats = client.getRotationStats()
1811
+ * ```
1812
+ */
1813
+ static rotating(config) {
1814
+ return TCloudClient.rotating(config);
1815
+ }
1513
1816
  /**
1514
1817
  * Generate a new ephemeral wallet (without creating a client).
1515
1818
  *
@@ -1562,11 +1865,31 @@ function getClient(opts) {
1562
1865
  }
1563
1866
  return new TCloud({ baseURL: `${config.apiUrl}/v1`, apiKey: config.apiKey, model: config.defaultModel });
1564
1867
  }
1868
+ function requireApiKey(config) {
1869
+ if (!config.apiKey) {
1870
+ console.error("No API key. Run: tcloud login");
1871
+ process.exit(1);
1872
+ }
1873
+ return config.apiKey;
1874
+ }
1875
+ function optionalNumber(value) {
1876
+ if (value == null) return void 0;
1877
+ const parsed = Number(value);
1878
+ if (!Number.isFinite(parsed)) {
1879
+ throw new Error(`Expected a number, got ${String(value)}`);
1880
+ }
1881
+ return parsed;
1882
+ }
1883
+ function teeType(value) {
1884
+ if (!value) return void 0;
1885
+ return value.toLowerCase();
1886
+ }
1565
1887
  var program = new import_commander.Command();
1566
1888
  program.name("tcloud").description("Tangle AI Cloud CLI").version("0.1.0");
1567
- program.command("config").description("View or update configuration").option("--api-url <url>", "API base URL").option("--api-key <key>", "API key").option("--model <model>", "Default model").option("--chain <id>", "Chain ID").action((opts) => {
1889
+ program.command("config").description("View or update configuration").option("--api-url <url>", "API base URL").option("--sandbox-url <url>", "Sandbox API base URL").option("--api-key <key>", "API key").option("--model <model>", "Default model").option("--chain <id>", "Chain ID").action((opts) => {
1568
1890
  const c = loadConfig();
1569
1891
  if (opts.apiUrl) c.apiUrl = opts.apiUrl;
1892
+ if (opts.sandboxUrl) c.sandboxUrl = opts.sandboxUrl;
1570
1893
  if (opts.apiKey) c.apiKey = opts.apiKey;
1571
1894
  if (opts.model) c.defaultModel = opts.model;
1572
1895
  if (opts.chain) c.chainId = parseInt(opts.chain);
@@ -1835,6 +2158,52 @@ program.command("operators").description("List active operators").action(async (
1835
2158
  console.error("Error:", e.message);
1836
2159
  }
1837
2160
  });
2161
+ var sandbox = program.command("sandbox").description("Sandbox workflows");
2162
+ sandbox.command("create").description("Create a sandbox").option("--name <name>", "Sandbox name").option("--image <image>", "Sandbox image or environment").option("--environment <environment>", "Sandbox environment").option("--ssh", "Enable SSH").option("--cpu <cores>", "CPU cores").option("--memory <mb>", "Memory in MB").option("--disk <gb>", "Disk in GB").option("--git-url <url>", "Git repository URL").option("--git-ref <ref>", "Git ref").option("--backend <type>", "Agent backend").option("--tee <type>", "Require a TEE backend: any, tdx, nitro, sev-snp, phala-dstack, gcp, azure").option("--sealed", "Require sealed secret support").option("--attestation-nonce <hex|auto>", "Attach a caller challenge nonce").option("--verify", "Verify returned attestation evidence (default when --tee is set)").option("--allow-unverified-hardware", "Allow structural attestation checks before vendor-root verification is available").option("--sandbox-url <url>", "Sandbox API base URL").option("--json", "Print JSON").action(async (opts) => {
2163
+ try {
2164
+ const config = loadConfig();
2165
+ const client = new TCloudSandbox({
2166
+ apiKey: requireApiKey(config),
2167
+ baseUrl: opts.sandboxUrl ?? config.sandboxUrl
2168
+ });
2169
+ const result = await client.create({
2170
+ name: opts.name,
2171
+ image: opts.image,
2172
+ environment: opts.environment,
2173
+ ssh: Boolean(opts.ssh),
2174
+ cpu: optionalNumber(opts.cpu),
2175
+ memoryMb: optionalNumber(opts.memory),
2176
+ diskGb: optionalNumber(opts.disk),
2177
+ gitUrl: opts.gitUrl,
2178
+ gitRef: opts.gitRef,
2179
+ backend: opts.backend,
2180
+ tee: teeType(opts.tee),
2181
+ sealed: Boolean(opts.sealed),
2182
+ attestationNonce: opts.attestationNonce,
2183
+ verify: Boolean(opts.verify || opts.tee),
2184
+ attestationPolicy: {
2185
+ allowUnverifiedHardware: Boolean(opts.allowUnverifiedHardware)
2186
+ }
2187
+ });
2188
+ if (opts.json) {
2189
+ console.log(JSON.stringify(result, null, 2));
2190
+ return;
2191
+ }
2192
+ const box = result.sandbox;
2193
+ console.log(`Sandbox created: ${box.id ?? "unknown"}`);
2194
+ if (box.status) console.log(`Status: ${box.status}`);
2195
+ if (opts.tee) {
2196
+ const status = result.attestationStatus;
2197
+ console.log(`TEE: ${opts.tee}`);
2198
+ console.log(`Attestation: ${status.verified ? "verified" : status.evidenceReturned ? "unverified" : "not returned"}`);
2199
+ console.log(`Nonce bound: ${status.nonceBound ? "yes" : "no"}`);
2200
+ if (result.attestationNonce) console.log(`Attestation nonce: ${result.attestationNonce}`);
2201
+ }
2202
+ } catch (e) {
2203
+ console.error("Error:", e.message);
2204
+ process.exit(1);
2205
+ }
2206
+ });
1838
2207
  var credits = program.command("credits").description("Credit management");
1839
2208
  credits.command("balance").description("Check balance").action(async () => {
1840
2209
  const client = getClient();
package/dist/cli.js CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TCloud
4
- } from "./chunk-577KIKFA.js";
4
+ } from "./chunk-MB4VK4MM.js";
5
+ import {
6
+ TCloudSandbox
7
+ } from "./chunk-DBIT227N.js";
5
8
  import {
6
9
  generateWallet
7
- } from "./chunk-A7AEPV2G.js";
8
- import "./chunk-B22AH4JH.js";
10
+ } from "./chunk-4ZUVGKVH.js";
11
+ import "./chunk-M5K3EFNP.js";
9
12
 
10
13
  // src/cli.ts
11
14
  import { Command } from "commander";
@@ -49,11 +52,31 @@ function getClient(opts) {
49
52
  }
50
53
  return new TCloud({ baseURL: `${config.apiUrl}/v1`, apiKey: config.apiKey, model: config.defaultModel });
51
54
  }
55
+ function requireApiKey(config) {
56
+ if (!config.apiKey) {
57
+ console.error("No API key. Run: tcloud login");
58
+ process.exit(1);
59
+ }
60
+ return config.apiKey;
61
+ }
62
+ function optionalNumber(value) {
63
+ if (value == null) return void 0;
64
+ const parsed = Number(value);
65
+ if (!Number.isFinite(parsed)) {
66
+ throw new Error(`Expected a number, got ${String(value)}`);
67
+ }
68
+ return parsed;
69
+ }
70
+ function teeType(value) {
71
+ if (!value) return void 0;
72
+ return value.toLowerCase();
73
+ }
52
74
  var program = new Command();
53
75
  program.name("tcloud").description("Tangle AI Cloud CLI").version("0.1.0");
54
- program.command("config").description("View or update configuration").option("--api-url <url>", "API base URL").option("--api-key <key>", "API key").option("--model <model>", "Default model").option("--chain <id>", "Chain ID").action((opts) => {
76
+ program.command("config").description("View or update configuration").option("--api-url <url>", "API base URL").option("--sandbox-url <url>", "Sandbox API base URL").option("--api-key <key>", "API key").option("--model <model>", "Default model").option("--chain <id>", "Chain ID").action((opts) => {
55
77
  const c = loadConfig();
56
78
  if (opts.apiUrl) c.apiUrl = opts.apiUrl;
79
+ if (opts.sandboxUrl) c.sandboxUrl = opts.sandboxUrl;
57
80
  if (opts.apiKey) c.apiKey = opts.apiKey;
58
81
  if (opts.model) c.defaultModel = opts.model;
59
82
  if (opts.chain) c.chainId = parseInt(opts.chain);
@@ -322,6 +345,52 @@ program.command("operators").description("List active operators").action(async (
322
345
  console.error("Error:", e.message);
323
346
  }
324
347
  });
348
+ var sandbox = program.command("sandbox").description("Sandbox workflows");
349
+ sandbox.command("create").description("Create a sandbox").option("--name <name>", "Sandbox name").option("--image <image>", "Sandbox image or environment").option("--environment <environment>", "Sandbox environment").option("--ssh", "Enable SSH").option("--cpu <cores>", "CPU cores").option("--memory <mb>", "Memory in MB").option("--disk <gb>", "Disk in GB").option("--git-url <url>", "Git repository URL").option("--git-ref <ref>", "Git ref").option("--backend <type>", "Agent backend").option("--tee <type>", "Require a TEE backend: any, tdx, nitro, sev-snp, phala-dstack, gcp, azure").option("--sealed", "Require sealed secret support").option("--attestation-nonce <hex|auto>", "Attach a caller challenge nonce").option("--verify", "Verify returned attestation evidence (default when --tee is set)").option("--allow-unverified-hardware", "Allow structural attestation checks before vendor-root verification is available").option("--sandbox-url <url>", "Sandbox API base URL").option("--json", "Print JSON").action(async (opts) => {
350
+ try {
351
+ const config = loadConfig();
352
+ const client = new TCloudSandbox({
353
+ apiKey: requireApiKey(config),
354
+ baseUrl: opts.sandboxUrl ?? config.sandboxUrl
355
+ });
356
+ const result = await client.create({
357
+ name: opts.name,
358
+ image: opts.image,
359
+ environment: opts.environment,
360
+ ssh: Boolean(opts.ssh),
361
+ cpu: optionalNumber(opts.cpu),
362
+ memoryMb: optionalNumber(opts.memory),
363
+ diskGb: optionalNumber(opts.disk),
364
+ gitUrl: opts.gitUrl,
365
+ gitRef: opts.gitRef,
366
+ backend: opts.backend,
367
+ tee: teeType(opts.tee),
368
+ sealed: Boolean(opts.sealed),
369
+ attestationNonce: opts.attestationNonce,
370
+ verify: Boolean(opts.verify || opts.tee),
371
+ attestationPolicy: {
372
+ allowUnverifiedHardware: Boolean(opts.allowUnverifiedHardware)
373
+ }
374
+ });
375
+ if (opts.json) {
376
+ console.log(JSON.stringify(result, null, 2));
377
+ return;
378
+ }
379
+ const box = result.sandbox;
380
+ console.log(`Sandbox created: ${box.id ?? "unknown"}`);
381
+ if (box.status) console.log(`Status: ${box.status}`);
382
+ if (opts.tee) {
383
+ const status = result.attestationStatus;
384
+ console.log(`TEE: ${opts.tee}`);
385
+ console.log(`Attestation: ${status.verified ? "verified" : status.evidenceReturned ? "unverified" : "not returned"}`);
386
+ console.log(`Nonce bound: ${status.nonceBound ? "yes" : "no"}`);
387
+ if (result.attestationNonce) console.log(`Attestation nonce: ${result.attestationNonce}`);
388
+ }
389
+ } catch (e) {
390
+ console.error("Error:", e.message);
391
+ process.exit(1);
392
+ }
393
+ });
325
394
  var credits = program.command("credits").description("Credit management");
326
395
  credits.command("balance").description("Check balance").action(async () => {
327
396
  const client = getClient();
@@ -590,6 +590,8 @@ declare class PrivateRouter {
590
590
  private usage;
591
591
  private currentIndex;
592
592
  private totalRequests;
593
+ /** Slug of the most-recently-selected operator. Populated on each selectOperator() hit. */
594
+ private _lastSelectedSlug;
593
595
  constructor(config?: Partial<PrivateRouterConfig>);
594
596
  /** Set the available operator pool */
595
597
  setOperators(operators: OperatorInfo[]): void;
@@ -614,6 +616,8 @@ declare class PrivateRouter {
614
616
  private minExposure;
615
617
  private latencyAware;
616
618
  private recordUsage;
619
+ /** Slug of the most-recently-selected operator (null before the first call). */
620
+ get lastSelectedSlug(): string | null;
617
621
  private getLastUsedOperator;
618
622
  private peekNextOperator;
619
623
  }
@@ -623,6 +627,35 @@ declare class PrivateRouter {
623
627
  * Shared between CLI and SDK.
624
628
  */
625
629
 
630
+ /** Rotation knobs for {@link TCloudClient.rotating}. */
631
+ interface RotatingRoutingConfig {
632
+ /** Router strategy. Defaults to `'min-exposure'`. */
633
+ strategy?: Extract<RoutingStrategy, 'min-exposure' | 'round-robin' | 'random'>;
634
+ /**
635
+ * Pre-seed the operator pool. When omitted the client fetches
636
+ * `/api/operators` on the first call (TTL-cached).
637
+ */
638
+ pool?: OperatorInfo[];
639
+ /** Minimum distinct operators required before routing proceeds. */
640
+ minOperators?: number;
641
+ /** Max requests per operator before forced rotation. */
642
+ maxRequestsPerOperator?: number;
643
+ /** Exclude specific operator slugs. */
644
+ excludeOperators?: string[];
645
+ /** Prefer specific regions (others kept as fallback). */
646
+ preferRegions?: string[];
647
+ }
648
+ /** Configuration accepted by {@link TCloudClient.rotating}. */
649
+ type RotatingClientConfig = Omit<TCloudConfig, 'routing'> & {
650
+ routing?: RotatingRoutingConfig;
651
+ };
652
+ /** Rotation stats surfaced by {@link TCloudClient.getRotationStats}. */
653
+ interface RotationStats {
654
+ /** Per-operator call counter. */
655
+ callsByOperator: Record<string, number>;
656
+ /** Slug of the most-recently-selected operator, if any. */
657
+ currentOperator: string | null;
658
+ }
626
659
  declare class TCloudClient {
627
660
  readonly baseURL: string;
628
661
  readonly platformURL: string;
@@ -675,6 +708,28 @@ declare class TCloudClient {
675
708
  /** Optional config passthrough (timeout, retry, etc). */
676
709
  config?: Omit<TCloudConfig, 'apiKey' | 'baseURL'>;
677
710
  }): TCloudClient;
711
+ /**
712
+ * Build a client that rotates which operator serves each call. Mirrors
713
+ * {@link TCloudClient.shielded} in shape: returns a standard `TCloudClient`
714
+ * that behaves identically for the OpenAI-compatible surface but
715
+ * dispatches each chat/completions/embeddings request through a
716
+ * {@link PrivateRouter} — different operator per call per the chosen
717
+ * strategy.
718
+ *
719
+ * ```ts
720
+ * const tcloud = TCloudClient.rotating({
721
+ * apiKey: process.env.TCLOUD_API_KEY,
722
+ * routing: { strategy: 'min-exposure' },
723
+ * })
724
+ * await tcloud.ask('hello')
725
+ * tcloud.getRotationStats() // { callsByOperator: { ... }, currentOperator: '…' }
726
+ * ```
727
+ *
728
+ * Rotation is meaningful only for stateless calls. Sandbox-harness
729
+ * sessions bind to a single operator for the lifetime of the session;
730
+ * `rotating()` clients refuse to dispatch them — see {@link bridge}.
731
+ */
732
+ static rotating(config?: RotatingClientConfig): TCloudClient;
678
733
  constructor(config?: TCloudConfig);
679
734
  /** Set the SpendAuth signer for private mode */
680
735
  setSpendAuthSigner(fn: () => Promise<SpendAuth>): void;
@@ -754,8 +809,19 @@ declare class TCloudClient {
754
809
  *
755
810
  * Sessions persist across process restarts — use the same `resume` id
756
811
  * to land on the same CLI conversation (context intact, no replay tax).
812
+ *
813
+ * Guard: clients built via {@link TCloudClient.rotating} cannot dispatch
814
+ * sandbox-harness sessions (rotation rotates per call; a sandbox session
815
+ * binds to one operator). Attempting `bridge({ harness: 'sandbox' })` on
816
+ * a rotating client throws.
757
817
  */
758
818
  bridge(cfg: BridgeOptions): BridgeSession;
819
+ /**
820
+ * Rotation stats — populated only on clients created via
821
+ * {@link TCloudClient.rotating}. Non-rotating clients return an empty
822
+ * counter and `currentOperator: null`.
823
+ */
824
+ getRotationStats(): RotationStats;
759
825
  /** Convenience: send a single message and get the text response */
760
826
  ask(message: string, modelOrOptions?: string | Partial<ChatOptions>): Promise<string>;
761
827
  /** Convenience: send a single message and get the full completion (with usage) */
@@ -1149,4 +1215,4 @@ declare class TCloudError extends Error {
1149
1215
  constructor(status: number, message: string);
1150
1216
  }
1151
1217
 
1152
- export { type ApiKeyInfo as A, type BatchJobResponse as B, type ChatCompletion as C, type RoutingStrategy as D, type EmbeddingOptions as E, type FineTuningJob as F, type GatewayOptions as G, type SpendAuth as H, type ImageGenerateOptions as I, type JobEvent as J, type SpendingLimits as K, TCloudError as L, type Model as M, type TierConfig as N, type Operator as O, type PricingTier as P, type TranscriptionResponse as Q, type RerankOptions as R, type ShieldedConfig as S, TCloudClient as T, type UpdateKeyOptions as U, type VideoGenerateOptions as V, type VideoResponse as W, type WatchJobOptions as X, type TCloudConfig as a, type AvatarGenerateRequest as b, type AvatarGenerateResponse as c, type AvatarJobStatus as d, type AvatarResult as e, type BatchRequest as f, type BridgeOptions as g, BridgeSession as h, type ChatCompletionChunk as i, type ChatMessage as j, type ChatOptions as k, type CompletionOptions as l, type CompletionResponse as m, type CreateKeyOptions as n, type CreatedKey as o, type CreditBalance as p, type EmbeddingResponse as q, type FineTuningJobOptions as r, type ImageResponse as s, type OperatorInfo as t, type PrivacyConfig as u, PrivateRouter as v, type PrivateRouterConfig as w, type RerankResponse as x, type RetryConfig as y, type RoutingConfig as z };
1218
+ export { type ApiKeyInfo as A, type BatchJobResponse as B, type ChatCompletion as C, type RotatingRoutingConfig as D, type EmbeddingOptions as E, type FineTuningJob as F, type GatewayOptions as G, type RotationStats as H, type ImageGenerateOptions as I, type JobEvent as J, type RoutingConfig as K, type RoutingStrategy as L, type Model as M, type SpendAuth as N, type Operator as O, type PricingTier as P, type SpendingLimits as Q, type RotatingClientConfig as R, type ShieldedConfig as S, TCloudClient as T, TCloudError as U, type TierConfig as V, type TranscriptionResponse as W, type UpdateKeyOptions as X, type VideoGenerateOptions as Y, type VideoResponse as Z, type WatchJobOptions as _, type TCloudConfig as a, type AvatarGenerateRequest as b, type AvatarGenerateResponse as c, type AvatarJobStatus as d, type AvatarResult as e, type BatchRequest as f, type BridgeOptions as g, BridgeSession as h, type ChatCompletionChunk as i, type ChatMessage as j, type ChatOptions as k, type CompletionOptions as l, type CompletionResponse as m, type CreateKeyOptions as n, type CreatedKey as o, type CreditBalance as p, type EmbeddingResponse as q, type FineTuningJobOptions as r, type ImageResponse as s, type OperatorInfo as t, type PrivacyConfig as u, PrivateRouter as v, type PrivateRouterConfig as w, type RerankOptions as x, type RerankResponse as y, type RetryConfig as z };