@huaweidevtools/aishell-dsh-e2b 0.0.15 → 0.0.16

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/lib/index.js CHANGED
@@ -901,10 +901,9 @@ defineMethod("transform", [
901
901
  /**
902
902
  * How many pages of a list response to follow before giving up.
903
903
  *
904
- * The gateway pages at 100 and reports more with a cursor, so this is a bound
905
- * of 1000 sandboxes. It exists to stop a filter that matches nothing from
906
- * walking an entire account one page at a time; no deployment this plugin is
907
- * used against should reach it.
904
+ * Bounds one listing at 1000 sandboxes. A filter that matches nothing would
905
+ * otherwise walk an entire account one page at a time; no deployment this
906
+ * plugin is used against should reach the cap.
908
907
  */
909
908
  const MAX_LIST_PAGES = 10;
910
909
  /**
@@ -964,6 +963,41 @@ var SandboxClient = class {
964
963
  return sandbox;
965
964
  }
966
965
  /**
966
+ * Walk the sandbox listing, following the pagination cursor.
967
+ *
968
+ * ## Why it follows the cursor
969
+ *
970
+ * The gateway pages at 100 and reports more with a cursor. A single page is
971
+ * enough for the common case and wrong for a busy account — and wrong in the
972
+ * direction that matters: the caller is told "these are all of them" when
973
+ * they are the first hundred, and a metadata search for one specific sandbox
974
+ * reports "none" because that sandbox happens to be on a later page.
975
+ *
976
+ * ## Why the bound and the sequential awaits
977
+ *
978
+ * `MAX_LIST_PAGES` caps a filter that matches nothing, which would otherwise
979
+ * walk the whole account one page at a time. The awaits are sequential by
980
+ * necessity: each page's cursor comes from the previous response, so the
981
+ * pages cannot be requested together.
982
+ *
983
+ * Shared by both listing calls so the two cannot drift in either respect.
984
+ *
985
+ * @param apiKey - the key to present.
986
+ * @param query - the SDK query, when one narrows the listing.
987
+ * @returns every row the walk collected.
988
+ */
989
+ async collectPages(apiKey, query) {
990
+ const paginator = Sandbox.list({
991
+ apiKey,
992
+ domain: this.options.resolveDomain(),
993
+ requestTimeoutMs: this.options.resolveRequestTimeoutMs(),
994
+ ...query
995
+ });
996
+ const collected = [];
997
+ for (let page = 0; page < MAX_LIST_PAGES && paginator.hasNext; page += 1) collected.push(...await paginator.nextItems());
998
+ return collected;
999
+ }
1000
+ /**
967
1001
  * Find the sandboxes carrying one metadata pair.
968
1002
  *
969
1003
  * The SDK encodes the pair into the `metadata` query parameter and the
@@ -983,19 +1017,7 @@ var SandboxClient = class {
983
1017
  * @returns one row per matching sandbox.
984
1018
  */
985
1019
  async findByMetadata(metadata, signal) {
986
- const domain = this.options.resolveDomain();
987
- const requestTimeoutMs = this.options.resolveRequestTimeoutMs();
988
- const items = await this.runWithKeyRefresh(signal, async (apiKey) => {
989
- const paginator = Sandbox.list({
990
- apiKey,
991
- domain,
992
- requestTimeoutMs,
993
- query: { metadata }
994
- });
995
- const found = [];
996
- for (let page = 0; page < MAX_LIST_PAGES && paginator.hasNext; page += 1) found.push(...await paginator.nextItems());
997
- return found;
998
- });
1020
+ const items = await this.runWithKeyRefresh(signal, (apiKey) => this.collectPages(apiKey, { query: { metadata } }));
999
1021
  return items.map((item) => ({
1000
1022
  sandboxId: item.sandboxId,
1001
1023
  state: item.state
@@ -1012,18 +1034,7 @@ var SandboxClient = class {
1012
1034
  * @returns one row per sandbox, in the shape the tool renders.
1013
1035
  */
1014
1036
  async list(signal) {
1015
- const domain = this.options.resolveDomain();
1016
- const requestTimeoutMs = this.options.resolveRequestTimeoutMs();
1017
- const items = await this.runWithKeyRefresh(signal, async (apiKey) => {
1018
- const paginator = Sandbox.list({
1019
- apiKey,
1020
- domain,
1021
- requestTimeoutMs
1022
- });
1023
- const all = [];
1024
- for (let page = 0; page < MAX_LIST_PAGES && paginator.hasNext; page += 1) all.push(...await paginator.nextItems());
1025
- return all;
1026
- });
1037
+ const items = await this.runWithKeyRefresh(signal, (apiKey) => this.collectPages(apiKey, {}));
1027
1038
  return items.map((item) => ({
1028
1039
  sandboxId: item.sandboxId,
1029
1040
  templateId: item.templateId,
@@ -1196,6 +1207,7 @@ function toStoredSection(key) {
1196
1207
  /** One key, read from the store and replaced when it expires or is refused. */
1197
1208
  var SandboxKeySource = class {
1198
1209
  constructor(options) {
1210
+ this.generation = 0;
1199
1211
  this.options = options;
1200
1212
  this.fetchImpl = options.fetchImpl ?? fetch;
1201
1213
  }
@@ -1233,13 +1245,33 @@ var SandboxKeySource = class {
1233
1245
  * Forget the key, so the next {@link get} creates a new one.
1234
1246
  *
1235
1247
  * Called when the gateway refuses the key as unauthenticated — the one
1236
- * signal that it has stopped working earlier than its expiry said. The
1237
- * replacement is a different key, and therefore a different tenant: sandboxes
1238
- * created under the old one stop being reachable through this plugin.
1248
+ * signal that it has stopped working earlier than its expiry said — and when
1249
+ * the stored credentials change.
1250
+ *
1251
+ * The replacement is a different key, and therefore a different tenant:
1252
+ * sandboxes created under the old one stop being reachable through this
1253
+ * plugin.
1254
+ *
1255
+ * ## Why the generation is bumped and `pending` is not cleared
1256
+ *
1257
+ * Clearing `pending` would let a concurrent caller start a second create
1258
+ * immediately, which is two keys and two tenants — the thing the shared
1259
+ * promise exists to prevent. Bumping the generation instead leaves the
1260
+ * in-flight create shared, and makes it fail when it lands: its key was
1261
+ * minted under credentials that no longer exist, so adopting it would put a
1262
+ * credential granted under a cleared account back into the store.
1263
+ *
1264
+ * The clear is best-effort. A store that refuses the delete still has the key
1265
+ * dropped from memory, and the fingerprint check rejects it on the next read
1266
+ * anyway — throwing here would turn a refresh into a failure the caller
1267
+ * cannot act on.
1239
1268
  */
1240
1269
  invalidate() {
1241
1270
  this.current = void 0;
1242
- clearE2BKey(this.options.store);
1271
+ this.generation += 1;
1272
+ try {
1273
+ clearE2BKey(this.options.store);
1274
+ } catch {}
1243
1275
  }
1244
1276
  /**
1245
1277
  * Create a key, store it, and return it.
@@ -1247,9 +1279,12 @@ var SandboxKeySource = class {
1247
1279
  * @param credentials - the pair to sign with; already read by {@link get},
1248
1280
  * so the fingerprint recorded on the key is the one it was minted under.
1249
1281
  * @param signal - caller cancellation, forwarded to the fetch.
1282
+ * @throws {SandboxKeyError} when the credentials changed while the request
1283
+ * was in flight, so the key that came back belongs to none of them.
1250
1284
  */
1251
1285
  async createKey(credentials, signal) {
1252
1286
  if (credentials === void 0) throw new SandboxKeyError("未进行华为云 IAM 认证,请认证后重试。 / Not authenticated with Huawei Cloud IAM. Please authenticate, then retry.");
1287
+ const generation = this.generation;
1253
1288
  const url = new URL(`${this.options.baseUrl}${KEYS_PATH}`);
1254
1289
  const headers = sign("POST", url.host, url.pathname, {}, "", credentials);
1255
1290
  const timeout = AbortSignal.timeout(this.options.timeoutMs);
@@ -1274,9 +1309,10 @@ var SandboxKeySource = class {
1274
1309
  ...await readCreatedKey(response),
1275
1310
  akFingerprint: fingerprint(credentials.ak)
1276
1311
  };
1312
+ if (generation !== this.generation) throw new SandboxKeyError("aishell-e2b: the Huawei Cloud credentials changed while a sandbox key was being created, so that key belongs to the previous account and was discarded. Retry with the current credentials.");
1277
1313
  try {
1278
1314
  writeE2BKey(this.options.store, toStoredSection(created));
1279
- } catch (error) {}
1315
+ } catch {}
1280
1316
  return created;
1281
1317
  }
1282
1318
  };
@@ -1579,16 +1615,37 @@ function describeInfo(info) {
1579
1615
  ...info.metadata === void 0 ? {} : { metadata: info.metadata }
1580
1616
  };
1581
1617
  }
1582
- /** How much of a command's output a result carries before it is elided. */
1618
+ /**
1619
+ * How much of a command's output a result carries before it is elided.
1620
+ *
1621
+ * Counted in code points, which is what a reader means by "characters" and what
1622
+ * the context budget is actually spent in. A UTF-16 code unit is a third of the
1623
+ * cost for a three-byte character, so measuring in units would let 30 000 CJK
1624
+ * characters through as 90 KB of text.
1625
+ */
1583
1626
  const OUTPUT_LIMIT = 3e4;
1584
- /** Keep a stream useful without letting one command flood the context. */
1627
+ /**
1628
+ * Keep a stream useful without letting one command flood the context.
1629
+ *
1630
+ * Truncation walks code points rather than slicing at a unit index: a slice at
1631
+ * an odd offset inside a surrogate pair leaves a lone surrogate, which is not
1632
+ * valid text and renders as a replacement character at the cut.
1633
+ *
1634
+ * @param text - the stream as received.
1635
+ * @returns the text, and whether any of it was dropped.
1636
+ */
1585
1637
  function clip(text) {
1586
1638
  if (text.length <= OUTPUT_LIMIT) return {
1587
1639
  text,
1588
1640
  truncated: false
1589
1641
  };
1642
+ const points = [...text];
1643
+ if (points.length <= OUTPUT_LIMIT) return {
1644
+ text,
1645
+ truncated: false
1646
+ };
1590
1647
  return {
1591
- text: text.slice(0, OUTPUT_LIMIT),
1648
+ text: points.slice(0, OUTPUT_LIMIT).join(""),
1592
1649
  truncated: true
1593
1650
  };
1594
1651
  }
@@ -68,6 +68,31 @@ export declare class SandboxClient {
68
68
  readonly metadata?: Record<string, string>;
69
69
  readonly signal?: AbortSignal;
70
70
  }): Promise<Sandbox>;
71
+ /**
72
+ * Walk the sandbox listing, following the pagination cursor.
73
+ *
74
+ * ## Why it follows the cursor
75
+ *
76
+ * The gateway pages at 100 and reports more with a cursor. A single page is
77
+ * enough for the common case and wrong for a busy account — and wrong in the
78
+ * direction that matters: the caller is told "these are all of them" when
79
+ * they are the first hundred, and a metadata search for one specific sandbox
80
+ * reports "none" because that sandbox happens to be on a later page.
81
+ *
82
+ * ## Why the bound and the sequential awaits
83
+ *
84
+ * `MAX_LIST_PAGES` caps a filter that matches nothing, which would otherwise
85
+ * walk the whole account one page at a time. The awaits are sequential by
86
+ * necessity: each page's cursor comes from the previous response, so the
87
+ * pages cannot be requested together.
88
+ *
89
+ * Shared by both listing calls so the two cannot drift in either respect.
90
+ *
91
+ * @param apiKey - the key to present.
92
+ * @param query - the SDK query, when one narrows the listing.
93
+ * @returns every row the walk collected.
94
+ */
95
+ private collectPages;
71
96
  /**
72
97
  * Find the sandboxes carrying one metadata pair.
73
98
  *
@@ -1,19 +1,17 @@
1
1
  /**
2
2
  * Reading the operator's Huawei Cloud credentials for control-plane requests.
3
3
  *
4
- * The read is delegated to `credentials-store/accounts`, which is now the one
5
- * place the account names and the pairing rule live. Every plugin in this set
6
- * that signs with the same key pair goes through it — this module, the LLM
7
- * adapter, the credential form's Host route and the ops tools — so they cannot
8
- * disagree about which keyring entries are the credentials or about what
9
- * "half a pair" means.
4
+ * The read is delegated to `credentials-store/accounts`, the one place the
5
+ * account names and the pairing rule live. Every plugin in this set that signs
6
+ * with the same key pair goes through it — this module, the LLM adapter, the
7
+ * credential form's Host route and the ops tools — so they cannot disagree
8
+ * about which keyring entries are the credentials, or about what "half a pair"
9
+ * means.
10
10
  *
11
- * It used to say here that the literals were re-declared because
12
- * `aishell-credentials` kept them unexported and the other packages each had
13
- * their own copy. That was an accurate description of a real hazard, not a
14
- * reason to keep it: a divergence in these names would not fail a build, it
15
- * would surface as one plugin working and another reporting "not
16
- * authenticated" on a machine where the operator had stored exactly one pair.
11
+ * Those names are a persistence contract: a second copy of them does not fail a
12
+ * build, it surfaces as one plugin working while another reports "not
13
+ * authenticated" on a machine where the operator has stored exactly one pair.
14
+ * One shared definition is what rules that out.
17
15
  *
18
16
  * @module @huaweidevtools/aishell-dsh-e2b/credentials
19
17
  */
@@ -10,15 +10,14 @@
10
10
  *
11
11
  * ## Why the split is stated three ways
12
12
  *
13
- * An earlier version drew the line at lifecycle alone: "does the result need to
14
- * outlive the task?" That is the property that actually decides the tier, but it
15
- * is one the model has to *infer* about work that has not been described in
16
- * those terms yet.
17
- *
18
- * Two things make it answerable instead:
13
+ * The deciding property is lifecycle — does the result need to outlive the
14
+ * task? — but that is one the model has to *infer* about work nobody has
15
+ * described in those terms yet. Two more signals make the same question
16
+ * answerable directly:
19
17
  *
20
18
  * - **What the user asked for.** "A web page or a demo" versus "a production
21
- * system" is a distinction a person can answer directly.
19
+ * system" is a distinction a person can answer without knowing what a
20
+ * sandbox is.
22
21
  * - **What the work needs.** The one capability a sandbox has that this
23
22
  * machine does not is a public address. A request to deploy something and
24
23
  * get a URL back is the strongest signal there is, and it is stated first
@@ -33,13 +33,12 @@
33
33
  *
34
34
  * ## Why the key is bound to the AK that minted it
35
35
  *
36
- * A stored key outlives the credentials that created it. Clearing or replacing
37
- * the AK/SK in the credential form therefore used to leave the old key — and so
38
- * the old tenant, with all its sandboxes — alive and usable, which contradicts
39
- * what clearing credentials is supposed to mean. A key with no expiry made that
40
- * permanent rather than temporary.
36
+ * A stored key outlives the credentials that created it, so clearing or
37
+ * replacing the AK/SK in the credential form has to take the key with it — a
38
+ * key with no expiry otherwise survives the credential for good, and the old
39
+ * tenant and all its sandboxes stay reachable.
41
40
  *
42
- * So the key records a fingerprint of the AK that minted it, and {@link get}
41
+ * The key records a fingerprint of the AK that minted it, and {@link get}
43
42
  * discards a stored key whose fingerprint no longer matches the stored
44
43
  * credentials. Three consequences are deliberate:
45
44
  *
@@ -143,6 +142,17 @@ export declare class SandboxKeySource {
143
142
  private current;
144
143
  /** The in-flight create, so concurrent callers share one request. */
145
144
  private pending;
145
+ /**
146
+ * Bumped by {@link invalidate}.
147
+ *
148
+ * A create signs with the credentials it was handed, and the network round
149
+ * trip is long enough to outlast them: a person can clear or replace their
150
+ * AK/SK while a key is being minted. The generation is how the finished
151
+ * create notices — its value is the one captured before the request, so a
152
+ * mismatch means the credentials it signed under are gone and the key it is
153
+ * holding must not be adopted or stored.
154
+ */
155
+ private generation;
146
156
  constructor(options: SandboxKeyOptions);
147
157
  /**
148
158
  * Return a usable API key.
@@ -161,9 +171,26 @@ export declare class SandboxKeySource {
161
171
  * Forget the key, so the next {@link get} creates a new one.
162
172
  *
163
173
  * Called when the gateway refuses the key as unauthenticated — the one
164
- * signal that it has stopped working earlier than its expiry said. The
165
- * replacement is a different key, and therefore a different tenant: sandboxes
166
- * created under the old one stop being reachable through this plugin.
174
+ * signal that it has stopped working earlier than its expiry said — and when
175
+ * the stored credentials change.
176
+ *
177
+ * The replacement is a different key, and therefore a different tenant:
178
+ * sandboxes created under the old one stop being reachable through this
179
+ * plugin.
180
+ *
181
+ * ## Why the generation is bumped and `pending` is not cleared
182
+ *
183
+ * Clearing `pending` would let a concurrent caller start a second create
184
+ * immediately, which is two keys and two tenants — the thing the shared
185
+ * promise exists to prevent. Bumping the generation instead leaves the
186
+ * in-flight create shared, and makes it fail when it lands: its key was
187
+ * minted under credentials that no longer exist, so adopting it would put a
188
+ * credential granted under a cleared account back into the store.
189
+ *
190
+ * The clear is best-effort. A store that refuses the delete still has the key
191
+ * dropped from memory, and the fingerprint check rejects it on the next read
192
+ * anyway — throwing here would turn a refresh into a failure the caller
193
+ * cannot act on.
167
194
  */
168
195
  invalidate(): void;
169
196
  /**
@@ -172,6 +199,8 @@ export declare class SandboxKeySource {
172
199
  * @param credentials - the pair to sign with; already read by {@link get},
173
200
  * so the fingerprint recorded on the key is the one it was minted under.
174
201
  * @param signal - caller cancellation, forwarded to the fetch.
202
+ * @throws {SandboxKeyError} when the credentials changed while the request
203
+ * was in flight, so the key that came back belongs to none of them.
175
204
  */
176
205
  private createKey;
177
206
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huaweidevtools/aishell-dsh-e2b",
3
3
  "description": "Runs throwaway and preview deployments in E2B sandboxes on DSH — sandbox lifecycle, commands, and files as tools",
4
- "version": "0.0.15",
4
+ "version": "0.0.16",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,7 +27,7 @@
27
27
  "license": "Apache-2.0",
28
28
  "dependencies": {
29
29
  "e2b": "2.59.0",
30
- "@huaweidevtools/aishell-dsh-credentials-store": "0.0.15"
30
+ "@huaweidevtools/aishell-dsh-credentials-store": "0.0.16"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.4",