@tangle-network/sandbox 0.60.31-develop.20261010071853.e377e11 → 0.60.31-develop.20261010073257.5e0a52f

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.
@@ -63,11 +63,13 @@ interface OperatorQuote {
63
63
  operatorAddress: `0x${string}`;
64
64
  /** EIP-191 personal_sign over the canonical quote payload (0x RSV). */
65
65
  signature?: `0x${string}`;
66
- /** wei/second, from the operator's signed policy at request time. */
66
+ /** wei/second per device, from the operator's signed policy at request time. */
67
67
  pricePerSecond: string;
68
68
  durationSeconds: number;
69
69
  confidentiality: number;
70
- /** wei — pricePerSecond × durationSeconds exactly. */
70
+ /** Devices rented. The signed v2 payload binds this count. */
71
+ deviceCount: number;
72
+ /** wei — pricePerSecond × durationSeconds × deviceCount exactly. */
71
73
  escrowTotal: string;
72
74
  /** Utilization the price was struck at (bps of 10_000) — the surge "why". */
73
75
  utilizationBps: number;
@@ -155,12 +157,14 @@ declare function decodeGpuLeaseOutput(encoded: `0x${string}`): GpuLeaseOutputDec
155
157
  //#endregion
156
158
  //#region src/gpu-lease/quotes.d.ts
157
159
  /** The exact string the operator signs — mirrors the blueprint's
158
- * `canonical_quote_payload` byte-for-byte. Every priced field is bound. */
160
+ * `canonical_quote_payload` (v2) byte-for-byte. Every priced field is bound,
161
+ * the device count included. */
159
162
  declare function canonicalQuotePayload(quote: {
160
163
  gpuClass: string;
161
164
  pricePerSecond: string;
162
165
  durationSeconds: number;
163
166
  confidentiality: number;
167
+ deviceCount: number;
164
168
  escrowTotal: string;
165
169
  intentHash: string;
166
170
  validUntil: number;
@@ -175,6 +179,14 @@ type QuoteRejection = {
175
179
  kind: "class-mismatch";
176
180
  requested: string;
177
181
  quoted: string;
182
+ } | {
183
+ kind: "duration-mismatch";
184
+ requested: number;
185
+ quoted: number;
186
+ } | {
187
+ kind: "count-mismatch";
188
+ requested: number;
189
+ quoted: number;
178
190
  } | {
179
191
  kind: "escrow-inconsistent";
180
192
  expected: bigint;
@@ -187,6 +199,10 @@ type QuoteRejection = {
187
199
  kind: "signature-mismatch";
188
200
  claimed: string;
189
201
  recovered: string;
202
+ } | {
203
+ kind: "operator-not-pinned";
204
+ pinned: string;
205
+ recovered: string;
190
206
  } | {
191
207
  kind: "unsigned";
192
208
  operator: string;
@@ -195,11 +211,13 @@ interface QuoteFilterInput {
195
211
  requestedClass: string;
196
212
  requestedDurationSeconds: number;
197
213
  requestedConfidentiality: number;
214
+ requestedDeviceCount: number;
198
215
  now?: () => number;
199
216
  }
200
217
  /**
201
218
  * Validate one quote. Fail-closed on every field the RFQ binds:
202
- * class, duration-derived escrow, confidentiality, and expiry.
219
+ * class, duration, device count, the escrow derived from them,
220
+ * confidentiality, and expiry.
203
221
  */
204
222
  declare function validateQuote(quote: OperatorQuote, input: QuoteFilterInput): ValidatedQuote | QuoteRejection;
205
223
  /**
@@ -226,9 +244,13 @@ declare function selectQuotes(quotes: {
226
244
  * invalid signature => reject (config-trust is dead on this path).
227
245
  * - no signature => the endpoint-config address is used (interim fallback);
228
246
  * strict mode rejects unsigned quotes outright.
247
+ * - `pinnedOperator` set => the quote must be signed AND recover to that
248
+ * address. A valid signature from any other key is rejected, so an
249
+ * endpoint cannot substitute its own signer for the one the caller trusts.
229
250
  */
230
251
  declare function validateQuoteIdentity(quote: OperatorQuote, opts?: {
231
252
  strict?: boolean;
253
+ pinnedOperator?: `0x${string}`;
232
254
  }): Promise<ValidatedQuote | QuoteRejection>;
233
255
  //#endregion
234
256
  //#region src/gpu-lease/vault.d.ts
@@ -130,9 +130,10 @@ function decodeGpuLeaseOutput(encoded) {
130
130
  * proven in blueprint #1568 and the operator policy's ceiling semantics.
131
131
  */
132
132
  /** The exact string the operator signs — mirrors the blueprint's
133
- * `canonical_quote_payload` byte-for-byte. Every priced field is bound. */
133
+ * `canonical_quote_payload` (v2) byte-for-byte. Every priced field is bound,
134
+ * the device count included. */
134
135
  function canonicalQuotePayload(quote) {
135
- return `gpu-lease-quote|v1|${quote.gpuClass}|${quote.pricePerSecond}|${quote.durationSeconds}|${quote.confidentiality}|${quote.escrowTotal}|${quote.intentHash}|${quote.validUntil}`;
136
+ return `gpu-lease-quote|v2|${quote.gpuClass}|${quote.pricePerSecond}|${quote.durationSeconds}|${quote.confidentiality}|${quote.deviceCount}|${quote.escrowTotal}|${quote.intentHash}|${quote.validUntil}`;
136
137
  }
137
138
  /** Offline EIP-191 recovery over the canonical payload. */
138
139
  async function verifyOperatorQuote(quote) {
@@ -143,7 +144,8 @@ async function verifyOperatorQuote(quote) {
143
144
  }
144
145
  /**
145
146
  * Validate one quote. Fail-closed on every field the RFQ binds:
146
- * class, duration-derived escrow, confidentiality, and expiry.
147
+ * class, duration, device count, the escrow derived from them,
148
+ * confidentiality, and expiry.
147
149
  */
148
150
  function validateQuote(quote, input) {
149
151
  const now = (input.now ?? Date.now)() / 1e3;
@@ -152,13 +154,23 @@ function validateQuote(quote, input) {
152
154
  requested: input.requestedClass,
153
155
  quoted: quote.gpuClass
154
156
  };
157
+ if (quote.durationSeconds !== input.requestedDurationSeconds) return {
158
+ kind: "duration-mismatch",
159
+ requested: input.requestedDurationSeconds,
160
+ quoted: quote.durationSeconds
161
+ };
162
+ if (quote.deviceCount !== input.requestedDeviceCount) return {
163
+ kind: "count-mismatch",
164
+ requested: input.requestedDeviceCount,
165
+ quoted: quote.deviceCount
166
+ };
155
167
  if (quote.confidentiality !== input.requestedConfidentiality) return {
156
168
  kind: "confidentiality-mismatch",
157
169
  requested: input.requestedConfidentiality,
158
170
  quoted: quote.confidentiality
159
171
  };
160
172
  const price = BigInt(quote.pricePerSecond);
161
- const expectedEscrow = price * BigInt(quote.durationSeconds);
173
+ const expectedEscrow = price * BigInt(quote.durationSeconds) * BigInt(quote.deviceCount);
162
174
  const actualEscrow = BigInt(quote.escrowTotal);
163
175
  if (expectedEscrow !== actualEscrow) return {
164
176
  kind: "escrow-inconsistent",
@@ -210,6 +222,9 @@ function selectQuotes(quotes, input) {
210
222
  * invalid signature => reject (config-trust is dead on this path).
211
223
  * - no signature => the endpoint-config address is used (interim fallback);
212
224
  * strict mode rejects unsigned quotes outright.
225
+ * - `pinnedOperator` set => the quote must be signed AND recover to that
226
+ * address. A valid signature from any other key is rejected, so an
227
+ * endpoint cannot substitute its own signer for the one the caller trusts.
213
228
  */
214
229
  async function validateQuoteIdentity(quote, opts = {}) {
215
230
  const base = {
@@ -225,9 +240,14 @@ async function validateQuoteIdentity(quote, opts = {}) {
225
240
  claimed,
226
241
  recovered: recovered ?? "unrecoverable"
227
242
  };
243
+ if (opts.pinnedOperator !== void 0 && recovered !== opts.pinnedOperator.toLowerCase()) return {
244
+ kind: "operator-not-pinned",
245
+ pinned: opts.pinnedOperator.toLowerCase(),
246
+ recovered
247
+ };
228
248
  return base;
229
249
  }
230
- if (opts.strict) return {
250
+ if (opts.strict || opts.pinnedOperator !== void 0) return {
231
251
  kind: "unsigned",
232
252
  operator: quote.operatorAddress
233
253
  };
@@ -312,6 +332,7 @@ async function gpuLease(options) {
312
332
  requestedClass: gpuClass,
313
333
  requestedDurationSeconds: durationSeconds,
314
334
  requestedConfidentiality: confidentiality,
335
+ requestedDeviceCount: 1,
315
336
  now: options.now
316
337
  });
317
338
  if (ranked.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox",
3
- "version": "0.60.31-develop.20261010071853.e377e11",
3
+ "version": "0.60.31-develop.20261010073257.5e0a52f",
4
4
  "description": "Client SDK for the Tangle Sandbox platform - build AI agent applications with dev containers",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -149,12 +149,12 @@
149
149
  "vitest": "^4.1.11",
150
150
  "ws": "^8.20.0",
151
151
  "yjs": "13.6.30",
152
- "@products/sandbox-test-utils": "0.0.0",
153
152
  "@repo/shared": "0.0.0",
154
- "@tangle-network/cli-agent-registry": "0.6.2",
153
+ "@products/sandbox-test-utils": "0.0.0",
155
154
  "@tangle-network/agent-profile-materialize": "0.22.2",
156
- "@tangle-network/runtime-contracts": "0.16.0",
155
+ "@tangle-network/cli-agent-registry": "0.6.2",
157
156
  "@tangle-network/sdk": "4.0.3",
157
+ "@tangle-network/runtime-contracts": "0.16.0",
158
158
  "@tangle-network/sse-core": "0.0.0"
159
159
  },
160
160
  "scripts": {