@agoric/cosmic-swingset 0.41.4-dev-3825031.0 → 0.41.4-dev-d1ef359.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/cosmic-swingset",
3
- "version": "0.41.4-dev-3825031.0+3825031",
3
+ "version": "0.41.4-dev-d1ef359.0+d1ef359",
4
4
  "description": "Agoric's Cosmos blockchain integration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,15 +22,15 @@
22
22
  "author": "Agoric",
23
23
  "license": "Apache-2.0",
24
24
  "dependencies": {
25
- "@agoric/builders": "0.1.1-dev-3825031.0+3825031",
26
- "@agoric/cosmos": "0.34.2-dev-3825031.0+3825031",
27
- "@agoric/deploy-script-support": "0.10.4-dev-3825031.0+3825031",
28
- "@agoric/internal": "0.3.3-dev-3825031.0+3825031",
29
- "@agoric/store": "0.9.3-dev-3825031.0+3825031",
30
- "@agoric/swing-store": "0.9.2-dev-3825031.0+3825031",
31
- "@agoric/swingset-vat": "0.32.3-dev-3825031.0+3825031",
32
- "@agoric/telemetry": "0.6.3-dev-3825031.0+3825031",
33
- "@agoric/vm-config": "0.1.1-dev-3825031.0+3825031",
25
+ "@agoric/builders": "0.1.1-dev-d1ef359.0+d1ef359",
26
+ "@agoric/cosmos": "0.34.2-dev-d1ef359.0+d1ef359",
27
+ "@agoric/deploy-script-support": "0.10.4-dev-d1ef359.0+d1ef359",
28
+ "@agoric/internal": "0.3.3-dev-d1ef359.0+d1ef359",
29
+ "@agoric/store": "0.9.3-dev-d1ef359.0+d1ef359",
30
+ "@agoric/swing-store": "0.9.2-dev-d1ef359.0+d1ef359",
31
+ "@agoric/swingset-vat": "0.32.3-dev-d1ef359.0+d1ef359",
32
+ "@agoric/telemetry": "0.6.3-dev-d1ef359.0+d1ef359",
33
+ "@agoric/vm-config": "0.1.1-dev-d1ef359.0+d1ef359",
34
34
  "@endo/bundle-source": "^3.5.1",
35
35
  "@endo/env-options": "^1.1.8",
36
36
  "@endo/errors": "^1.2.9",
@@ -52,8 +52,12 @@
52
52
  "tmp": "^0.2.1"
53
53
  },
54
54
  "devDependencies": {
55
+ "@agoric/kmarshal": "0.1.1-dev-d1ef359.0+d1ef359",
56
+ "@endo/eventual-send": "^1.2.8",
55
57
  "ava": "^5.3.0",
56
- "c8": "^10.1.2"
58
+ "better-sqlite3": "^9.1.1",
59
+ "c8": "^10.1.2",
60
+ "ses": "^1.10.0"
57
61
  },
58
62
  "publishConfig": {
59
63
  "access": "public"
@@ -73,5 +77,5 @@
73
77
  "typeCoverage": {
74
78
  "atLeast": 83.4
75
79
  },
76
- "gitHead": "38250315cedf68dc801b75bee867818a3846fae7"
80
+ "gitHead": "d1ef35965047ef31f0fa5451d4cfb8bd06c59247"
77
81
  }
package/src/chain-main.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  makeReadCachingStorage,
48
48
  } from './helpers/bufferedStorage.js';
49
49
  import stringify from './helpers/json-stable-stringify.js';
50
- import { launch } from './launch-chain.js';
50
+ import { launch, launchAndShareInternals } from './launch-chain.js';
51
51
  import { makeProcessValue } from './helpers/process-value.js';
52
52
  import {
53
53
  spawnSwingStoreExport,
@@ -228,7 +228,11 @@ export const makeQueueStorage = (call, queuePath) => {
228
228
  * slogSender?: ERef<EReturn<typeof makeSlogSender>>,
229
229
  * swingStore?: import('@agoric/swing-store').SwingStore,
230
230
  * vatconfig?: Parameters<typeof launch>[0]['vatconfig'],
231
- * }} [options.testingOverrides]
231
+ * withInternals?: boolean,
232
+ * }} [options.testingOverrides] Exposed only for testing purposes.
233
+ * `debugName`/`slogSender`/`swingStore`/`vatConfig` are pure overrides, while
234
+ * `withInternals` expands the return value to expose internal objects
235
+ * `controller`/`bridgeInbound`/`timer`.
232
236
  */
233
237
  export const makeLaunchChain = (
234
238
  agcc,
@@ -523,7 +527,10 @@ export const makeLaunchChain = (
523
527
  ? makeArchiveTranscript(vatTranscriptArchiveDir, fsPowers)
524
528
  : undefined;
525
529
 
526
- const s = await launch({
530
+ const launcher = testingOverrides.withInternals
531
+ ? launchAndShareInternals
532
+ : launch;
533
+ const s = await launcher({
527
534
  actionQueueStorage,
528
535
  highPriorityQueueStorage,
529
536
  swingStore: testingOverrides.swingStore,
@@ -76,10 +76,8 @@ export const makeKVStoreFromMap = map => {
76
76
  let priorKeyIndex;
77
77
 
78
78
  const ensureSorted = () => {
79
- if (!sortedKeys) {
80
- sortedKeys = [...map.keys()];
81
- sortedKeys.sort(compareByCodePoints);
82
- }
79
+ if (sortedKeys) return;
80
+ sortedKeys = [...map.keys()].sort(compareByCodePoints);
83
81
  };
84
82
 
85
83
  const clearGetNextKeyCache = () => {
@@ -101,9 +99,16 @@ export const makeKVStoreFromMap = map => {
101
99
  assert.typeof(priorKey, 'string');
102
100
  ensureSorted();
103
101
  const start =
104
- compareByCodePoints(priorKeyReturned, priorKey) <= 0
105
- ? priorKeyIndex + 1
106
- : 0;
102
+ priorKeyReturned === undefined
103
+ ? 0
104
+ : // If priorKeyReturned <= priorKey, start just after it.
105
+ (compareByCodePoints(priorKeyReturned, priorKey) <= 0 &&
106
+ priorKeyIndex + 1) ||
107
+ // Else if priorKeyReturned immediately follows priorKey, start at
108
+ // its index (and expect to return it again).
109
+ (sortedKeys.at(priorKeyIndex - 1) === priorKey && priorKeyIndex) ||
110
+ // Otherwise, start at the beginning.
111
+ 0;
107
112
  for (let i = start; i < sortedKeys.length; i += 1) {
108
113
  const key = sortedKeys[i];
109
114
  if (compareByCodePoints(key, priorKey) <= 0) continue;
@@ -226,7 +231,8 @@ export function makeBufferedStorage(kvStore, listeners = {}) {
226
231
 
227
232
  // To avoid confusion, additions and deletions are prevented from sharing
228
233
  // the same key at any given time.
229
- const additions = new Map();
234
+ /** @type {Map<string, T> & KVStore<T>} */
235
+ const additions = provideEnhancedKVStore(makeKVStoreFromMap(new Map()));
230
236
  const deletions = new Set();
231
237
 
232
238
  /** @type {KVStore<T>} */
@@ -257,13 +263,18 @@ export function makeBufferedStorage(kvStore, listeners = {}) {
257
263
  deletions.add(key);
258
264
  if (onPendingDelete !== undefined) onPendingDelete(key);
259
265
  },
260
-
261
- /**
262
- * @param {string} previousKey
263
- */
264
266
  getNextKey(previousKey) {
265
267
  assert.typeof(previousKey, 'string');
266
- throw Error('not implemented');
268
+ const bufferedNextKey = additions.getNextKey(previousKey);
269
+ let nextKey = kvStore.getNextKey(previousKey);
270
+ while (nextKey !== undefined) {
271
+ if (bufferedNextKey !== undefined) {
272
+ if (compareByCodePoints(bufferedNextKey, nextKey) <= 0) break;
273
+ }
274
+ if (!deletions.has(nextKey)) return nextKey;
275
+ nextKey = kvStore.getNextKey(nextKey);
276
+ }
277
+ return bufferedNextKey;
267
278
  },
268
279
  };
269
280
  function commit() {
@@ -26,8 +26,8 @@ import {
26
26
  } from '@agoric/swingset-vat';
27
27
  import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js';
28
28
  import { openSwingStore } from '@agoric/swing-store';
29
- import { BridgeId as BRIDGE_ID } from '@agoric/internal';
30
- import { objectMapMutable } from '@agoric/internal/src/js-utils.js';
29
+ import { attenuate, BridgeId as BRIDGE_ID } from '@agoric/internal';
30
+ import { objectMapMutable, TRUE } from '@agoric/internal/src/js-utils.js';
31
31
  import { makeWithQueue } from '@agoric/internal/src/queue.js';
32
32
  import * as ActionType from '@agoric/internal/src/action-types.js';
33
33
 
@@ -405,7 +405,7 @@ export async function buildSwingset(
405
405
  /**
406
406
  * @param {LaunchOptions} options
407
407
  */
408
- export async function launch({
408
+ export async function launchAndShareInternals({
409
409
  actionQueueStorage,
410
410
  highPriorityQueueStorage,
411
411
  kernelStateDBDir,
@@ -1408,5 +1408,26 @@ export async function launch({
1408
1408
  writeSlogObject,
1409
1409
  savedHeight,
1410
1410
  savedChainSends: JSON.parse(kvStore.get(getHostKey('chainSends')) || '[]'),
1411
+ // NOTE: to be used only for testing purposes!
1412
+ internals: {
1413
+ controller,
1414
+ bridgeInbound,
1415
+ timer,
1416
+ },
1411
1417
  };
1412
1418
  }
1419
+
1420
+ /**
1421
+ * @param {LaunchOptions} options
1422
+ * @returns {Promise<Omit<Awaited<ReturnType<typeof launchAndShareInternals>>, 'internals'>>}
1423
+ */
1424
+ export async function launch(options) {
1425
+ const launchResult = await launchAndShareInternals(options);
1426
+ return attenuate(launchResult, {
1427
+ blockingSend: TRUE,
1428
+ shutdown: TRUE,
1429
+ writeSlogObject: TRUE,
1430
+ savedHeight: TRUE,
1431
+ savedChainSends: TRUE,
1432
+ });
1433
+ }
@@ -0,0 +1,825 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file Interact with the database and/or vats of a swingstore.sqlite in an
4
+ * ephemeral environment. This file functions as both an importable module and
5
+ * as a standalone interactive or non-interactive script. See
6
+ * "Check for CLI invocation" below for usage detail about the latter.
7
+ */
8
+ /* eslint-env node */
9
+ /* global globalThis */
10
+ /* eslint-disable no-empty */
11
+
12
+ // Overwrite the global console for deeper inspection.
13
+ // @ts-expect-error TS2307 Cannot find module
14
+ import 'data:text/javascript,import { Console } from "node:console"; const { stdout, stderr, env } = process; const inspectOptions = { depth: Number(env.CONSOLE_INSPECT_DEPTH) || 6 }; globalThis.console = new Console({ stdout, stderr, inspectOptions });';
15
+
16
+ import 'ses';
17
+ import '@endo/eventual-send/shim.js';
18
+ import '@endo/init/pre.js';
19
+ // __hardenTaming__: "unsafe" is unfortunate, but without it, automatic
20
+ // hardening discovers EventEmitter.prototype and breaks creation of new event
21
+ // emitters (e.g., `Readable.from(...)`) because initialization is vulnerable to
22
+ // the property assignment override mistake w.r.t. _events/_eventsCount/etc.
23
+ // https://github.com/nodejs/node/blob/v22.12.0/lib/events.js#L347
24
+ // @ts-expect-error TS2307 Cannot find module
25
+ import 'data:text/javascript,try { lockdown({ domainTaming: "unsafe", errorTaming: "unsafe-debug", __hardenTaming__: "unsafe" }); } catch (_err) {}';
26
+
27
+ import { spawn } from 'node:child_process';
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+ import pathlib from 'node:path';
31
+ import repl from 'node:repl';
32
+ import stream from 'node:stream';
33
+ import {
34
+ setImmediate as resolveImmediate,
35
+ setTimeout as delay,
36
+ } from 'node:timers/promises';
37
+ import { fileURLToPath } from 'node:url';
38
+ import { inspect, parseArgs } from 'node:util';
39
+ import { isMainThread } from 'node:worker_threads';
40
+ // eslint-disable-next-line import/no-extraneous-dependencies
41
+ import sqlite3 from 'better-sqlite3';
42
+ import { Fail, b, q } from '@endo/errors';
43
+ import { makePromiseKit } from '@endo/promise-kit';
44
+ import { objectMap, BridgeId } from '@agoric/internal';
45
+ import { QueuedActionType } from '@agoric/internal/src/action-types.js';
46
+ import { defineName } from '@agoric/internal/src/js-utils.js';
47
+ import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js';
48
+ // eslint-disable-next-line import/no-extraneous-dependencies
49
+ import { krefOf, kser, kslot, kunser } from '@agoric/kmarshal';
50
+ import {
51
+ openSwingStore,
52
+ makeBundleStore,
53
+ bundleIDFromName,
54
+ } from '@agoric/swing-store';
55
+ import {
56
+ makeBufferedStorage,
57
+ provideEnhancedKVStore,
58
+ } from '../src/helpers/bufferedStorage.js';
59
+ import {
60
+ DEFAULT_SIM_SWINGSET_PARAMS,
61
+ makeVatCleanupBudgetFromKeywords,
62
+ } from '../src/sim-params.js';
63
+ import { makeCosmicSwingsetTestKit } from './test-kit.js';
64
+
65
+ /** @import { ManagerType, SwingSetConfig } from '@agoric/swingset-vat' */
66
+ /** @import { KVStore } from '../src/helpers/bufferedStorage.js' */
67
+
68
+ const useColors = process.stdout?.hasColors?.();
69
+ const inspectDepth = 6;
70
+
71
+ const dataProp = { writable: true, enumerable: true, configurable: true };
72
+ const empty = Object.create(null);
73
+ const noop = () => {};
74
+ const parseNumber = input => (input.match(/[0-9]/) ? Number(input) : NaN);
75
+
76
+ // cf. packages/swing-store/src/exporter.js
77
+ const storeExportAPI = ['getExportRecords', 'getArtifactNames'];
78
+
79
+ // TODO: getVatAdminNode('v112') # scan the vatAdmin vom v2.vs.vom.* vrefs for value matching /\b${vatID}\b/
80
+ export const makeHelpers = ({ db, EV }) => {
81
+ const sqlKVGet = db
82
+ .prepare('SELECT value FROM kvStore WHERE key = ?')
83
+ .pluck();
84
+ const kvGet = key => sqlKVGet.get(key);
85
+ const kvGetJSON = key => JSON.parse(kvGet(key));
86
+
87
+ const sqlKVByRange = db.prepare(
88
+ `SELECT key, value FROM kvStore WHERE key >= :a AND key < :b AND ${[
89
+ '(:keySuffix IS NULL OR substr(key, -length(:keySuffix)) = :keySuffix)',
90
+ '(:keyGlob IS NULL OR key GLOB :keyGlob)',
91
+ '(:valueGlob IS NULL OR value GLOB :valueGlob)',
92
+ ].join(' AND ')}`,
93
+ );
94
+ const sqlKVByHalfRange = db.prepare(
95
+ `SELECT key, value FROM kvStore WHERE key >= :a AND ${[
96
+ '(:keySuffix IS NULL OR substr(key, -length(:keySuffix)) = :keySuffix)',
97
+ '(:keyGlob IS NULL OR key GLOB :keyGlob)',
98
+ '(:valueGlob IS NULL OR value GLOB :valueGlob)',
99
+ ].join(' AND ')}`,
100
+ );
101
+ const kvGlob = (keyGlob, valueGlob = undefined, lazy = false) => {
102
+ const [_keyPattern, keyPrefix, keyTail, keySuffix] =
103
+ /** @type {string[]} */ (/^([^*?]*)((?:[*?]([^*?]*))*)$/.exec(keyGlob));
104
+ let sql = sqlKVByHalfRange;
105
+ /** @type {Record<'a' | 'b' | 'keySuffix' | 'keyGlob' | 'valueGlob', string | null>} */
106
+ const args = {
107
+ a: keyPrefix,
108
+ b: null,
109
+ keySuffix: keySuffix || null,
110
+ keyGlob: keyTail && keyTail !== '*' ? keyGlob : null,
111
+ valueGlob: valueGlob ?? null,
112
+ };
113
+ const chars = [...keyPrefix];
114
+ const i = chars.findLastIndex(ch => ch < '\u{10FFFF}');
115
+ if (i !== -1) {
116
+ sql = sqlKVByRange;
117
+ const newLastCP = /** @type {number} */ (chars[i].codePointAt(0)) + 1;
118
+ args.b = chars.slice(0, i).join('') + String.fromCodePoint(newLastCP);
119
+ } else {
120
+ console.warn('Warning: Unprefixed searches can be slow');
121
+ }
122
+ return lazy ? sql.iterate(args) : sql.all(args);
123
+ };
124
+
125
+ let vatsByID = new Map();
126
+ let vatsByName = new Map();
127
+ try {
128
+ // @see {@link ../../SwingSet/src/kernel/state/kernelKeeper.js}
129
+ kvGetJSON('vat.names').every(
130
+ name =>
131
+ typeof name === 'string' ||
132
+ Fail`static vat name ${q(name)} must be a string`,
133
+ );
134
+ kvGetJSON('vat.dynamicIDs').every(
135
+ vatID =>
136
+ typeof vatID === 'string' ||
137
+ Fail`dynamic vatID ${q(vatID)} must be a string`,
138
+ );
139
+ const vatQuery = db.prepare(`
140
+ WITH vat AS (
141
+ SELECT 1 AS rank, nameJSON.key AS idx, vatNameToID.value AS vatID
142
+ FROM kvStore AS nameList
143
+ LEFT JOIN json_each(nameList.value) AS nameJSON
144
+ LEFT JOIN kvStore AS vatNameToID
145
+ ON vatNameToID.key = 'vat.name.' || nameJSON.atom
146
+ WHERE nameList.key='vat.names'
147
+ UNION SELECT 2 as rank, idJSON.key AS idx, idJSON.value AS vatID
148
+ FROM kvStore AS idList, json_each(idList.value) AS idJSON
149
+ WHERE idList.key='vat.dynamicIDs'
150
+ )
151
+ SELECT vat.vatID, rank, source.value AS sourceText, options.value AS optionsText
152
+ FROM vat
153
+ LEFT JOIN kvStore AS source ON source.key = vat.vatID || '.source'
154
+ LEFT JOIN kvStore AS options ON options.key = vat.vatID || '.options'
155
+ ORDER BY vat.rank, vat.idx
156
+ `);
157
+ for (const dbRecord of vatQuery.iterate()) {
158
+ const { vatID, rank, sourceText, optionsText } = dbRecord;
159
+ const isStatic = rank === 1;
160
+ const source = sourceText ? JSON.parse(sourceText) : undefined;
161
+ const options = optionsText ? JSON.parse(optionsText) : undefined;
162
+ const name = options?.name;
163
+ const vat = harden({ vatID, name, isStatic, source, options });
164
+ vatsByID.set(vatID, vat);
165
+ if (name) {
166
+ const conflict = vatsByName.get(name);
167
+ if (vat.isStatic || !conflict) {
168
+ // Static vats trump dynamic vats in vatsByName.
169
+ vatsByName.set(name, vat);
170
+ } else if (!Array.isArray(conflict)) {
171
+ // ...but dynamic vats with duplicate names get collected into arrays.
172
+ vatsByName.set(name, [conflict, vat]);
173
+ } else {
174
+ conflict.push(vat);
175
+ }
176
+ }
177
+ }
178
+ } catch (err) {
179
+ console.warn('Warning: Could not build vat maps', err);
180
+ // @ts-expect-error
181
+ vatsByID = undefined;
182
+ // @ts-expect-error
183
+ vatsByName = undefined;
184
+ }
185
+
186
+ const vatIDPatt = /^v[1-9][0-9]*$/;
187
+ // @see {@link ../../SwingSet/docs/c-lists.md}
188
+ // @see {@link ../../swingset-liveslots/src/vatstore-usage.md}
189
+ const refPatt =
190
+ /(?<kref>^k[opd][1-9][0-9]*$)|(?<vref>^[opd][+-](?:0|[1-9][0-9]*)$|^(?<baseref>o[+][vd]?(?<kindID>[1-9][0-9]*)\/[1-9][0-9]*)(?<facetSuffix>:(?<facetID>0|[1-9][0-9]*))?)/;
191
+ const krefToVrefValuePatt = /^([R_]) ([^ ]+)$/;
192
+ const getKindMeta = (vatID, kindID) => {
193
+ const kindMetaJSON =
194
+ kvGet(`${vatID}.vs.vom.dkind.${kindID}.descriptor`) ||
195
+ kvGet(`${vatID}.vs.vom.vkind.${kindID}.descriptor`);
196
+ return JSON.parse(kindMetaJSON);
197
+ };
198
+
199
+ /**
200
+ * @param {string} refID kref or vref
201
+ * @param {string} [contextVatID]
202
+ * @returns {Array<{vatID: string, kref: string, vref: string, kind?: string, facet?: string}>}
203
+ */
204
+ const getRefs = (refID, contextVatID = undefined) => {
205
+ const refParts = refID.match(refPatt)?.groups;
206
+ if (!refParts) throw Fail`unknown kref or vref format in ${refID}`;
207
+ const isKref = !!refParts.kref;
208
+ contextVatID === undefined ||
209
+ contextVatID.match(vatIDPatt) ||
210
+ Fail`invalid contextVatID ${contextVatID}`;
211
+
212
+ // Search for rows like (`${vatID}.c.${kref}`, `${flag} ${vref}`), where
213
+ // kref might be exracted from rows like (`${vatID}.c.${vref}`, kref).
214
+ // @see {@link ../../SwingSet/docs/c-lists.md}
215
+ const krefs = [];
216
+ let kindMeta;
217
+ if (isKref) {
218
+ krefs.push(refID);
219
+ } else {
220
+ const maybeKref = kref => kref && krefs.push(kref);
221
+ maybeKref(kvGet(`${contextVatID}.c.${refID}`));
222
+ const { baseref, kindID, facetID } = refParts;
223
+ if (kindID && !facetID) {
224
+ // Each facet might have its own kref.
225
+ kindMeta = getKindMeta(contextVatID, kindID);
226
+ const facetNames = kindMeta?.facets;
227
+ for (let i = 0; i < (facetNames?.length ?? 0); i += 1) {
228
+ maybeKref(kvGet(`${contextVatID}.c.${baseref}:${i}`));
229
+ }
230
+ }
231
+ }
232
+ // Don't scan when we can enumerate keys.
233
+ const results = [];
234
+ for (const vatID of vatsByID.keys()) {
235
+ for (const kref of krefs) {
236
+ const value = kvGet(`${vatID}.c.${kref}`);
237
+ if (!value) continue;
238
+ const [_value, _reachabilityFlag, vref] =
239
+ value.match(krefToVrefValuePatt) ||
240
+ Fail`unexpected c-list value ${value}`;
241
+ const result = { vatID, kref, vref };
242
+ const { kindID, facetID } = vref.match(refPatt)?.groups || empty;
243
+ if (kindID) {
244
+ // kindID appears only in vrefs for the exporting vat, where we either
245
+ // get metadata on the first try or not at all.
246
+ if (kindMeta !== null) {
247
+ kindMeta ||= getKindMeta(vatID, kindID) || null;
248
+ }
249
+ result.kind = kindMeta?.tag;
250
+ if (facetID) result.facet = kindMeta?.facets?.[facetID];
251
+ }
252
+ results.push(result);
253
+ }
254
+ }
255
+ return results;
256
+ };
257
+
258
+ /**
259
+ * Run a core-eval directly through the controller (i.e., without a block).
260
+ *
261
+ * @param {string} fnText must evaluate to a function that will be invoked in
262
+ * a core eval compartment with a "powers" argument as attenuated by
263
+ * `permits` (with no attenuation by default).
264
+ * @param {import('@agoric/vats/src/core/lib-boot.js').BootstrapManifestPermit} [permits]
265
+ */
266
+ const runCoreEval = async (fnText, permits = true) => {
267
+ // Fail noisily if fnText does not evaluate to a function.
268
+ // This must be refactored if there is ever a need for such input.
269
+ const fn = new Compartment().evaluate(fnText);
270
+ typeof fn === 'function' || Fail`text must evaluate to a function`;
271
+ /** @type {import('@agoric/cosmic-proto/swingset/swingset.js').CoreEvalSDKType} */
272
+ const coreEvalDesc = {
273
+ json_permits: JSON.stringify(permits),
274
+ js_code: fnText,
275
+ };
276
+ const coreEvalAction = {
277
+ type: QueuedActionType.CORE_EVAL,
278
+ evals: [coreEvalDesc],
279
+ };
280
+ // Assume a path to the coreEvalBridgeHandler.
281
+ const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem(
282
+ 'coreEvalBridgeHandler',
283
+ );
284
+ return EV(coreEvalBridgeHandler).fromBridge(coreEvalAction);
285
+ };
286
+
287
+ return harden({
288
+ runCoreEval,
289
+ stable: { db, getRefs, kvGet, kvGetJSON, kvGlob, vatsByID, vatsByName },
290
+ });
291
+ };
292
+
293
+ /**
294
+ * Wrap a swing-store sub-store (kvStore/transcriptStore/etc.) with a
295
+ * replacement whose functions log and/or track/respond to staleness.
296
+ *
297
+ * @template {object} Substore
298
+ * @param {string} storeName
299
+ * @param {Substore} store
300
+ * @param {object} [options]
301
+ * @param {(...args: unknown[]) => void} [options.log]
302
+ * @param {(...args: unknown[]) => void} [options.warn]
303
+ * @param {(key?: unknown) => boolean} [options.isClean]
304
+ * @param {(key?: unknown) => boolean} [options.isStale]
305
+ * @param {(key?: unknown) => void} [options.markStale]
306
+ * @param {string[]} [options.allow] functions to allow
307
+ * @param {string[]} [options.allowIfClean] functions to allow when `isClean(firstArg)` returns true
308
+ * @param {string[]} [options.allowAndMark] functions to augment with `markStale(firstArg)`
309
+ * @param {Array<string | [string, Function]>} [options.logAndMark] functions to replace with `log(storeName, functionName, firstArg, ...details)` and `markStale(firstArg)`
310
+ * @param {string[]} [options.warnIfStale] functions to augment with `warn(storeName, functionName, firstArg, ...details)` when `isStale(firstArg)` returns true
311
+ * @param {string[]} [options.disallow] functions to disallow
312
+ * @returns {Substore}
313
+ */
314
+ export const wrapSubstore = (storeName, store, options = {}) => {
315
+ const {
316
+ log = console.log,
317
+ warn = console.warn,
318
+ isClean = () => Fail`[inquisitor] cannot check isClean in ${storeName}`,
319
+ isStale = () => Fail`[inquisitor] cannot check isStale in ${storeName}`,
320
+ markStale = () => Fail`[inquisitor] cannot markStale in ${storeName}`,
321
+ allow = [],
322
+ allowIfClean = [],
323
+ allowAndMark = [],
324
+ logAndMark: rawLogAndMark = [],
325
+ warnIfStale = [],
326
+ disallow = [],
327
+ } = options;
328
+ const logAndMarkMap = new Map(
329
+ rawLogAndMark.map(x => (Array.isArray(x) ? x : [x, noop])),
330
+ );
331
+ const flat = (...arrs) => [].concat(...arrs);
332
+ /** @type {Set<string>} */
333
+ const unseen = new Set(
334
+ flat(allow, allowIfClean, allowAndMark, warnIfStale, disallow),
335
+ );
336
+ for (const name of logAndMarkMap.keys()) unseen.add(name);
337
+ const wrapped = objectMap(
338
+ /** @type {Record<string, Function>} */ (store),
339
+ (fn, name) => {
340
+ if (typeof name !== 'string') {
341
+ throw Fail`[inquisitor] non-string property ${b(storeName)}[${q(name)}]`;
342
+ }
343
+ unseen.delete(name);
344
+ if (allow.includes(name)) {
345
+ return fn;
346
+ } else if (allowIfClean.includes(name)) {
347
+ return defineName(name, (key, ...rest) => {
348
+ isClean(key) ||
349
+ Fail`[inquisitor] ${b(storeName)}.${b(name)}(${b(key)}) after mutations`;
350
+ return fn(key, ...rest);
351
+ });
352
+ } else if (allowAndMark.includes(name)) {
353
+ return defineName(name, (key, ...rest) => {
354
+ markStale(key);
355
+ return fn(key, ...rest);
356
+ });
357
+ } else if (logAndMarkMap.has(name)) {
358
+ const makeResult = /** @type {Function} */ (logAndMarkMap.get(name));
359
+ return defineName(name, (key, ...rest) => {
360
+ markStale(key);
361
+ log(storeName, name, key, ...rest);
362
+ return makeResult(key, ...rest);
363
+ });
364
+ } else if (warnIfStale.includes(name)) {
365
+ return defineName(name, (key, ...rest) => {
366
+ if (isStale(key)) {
367
+ warn(storeName, name, key, 'returning stale data');
368
+ }
369
+ return fn(key, ...rest);
370
+ });
371
+ } else if (disallow.includes(name)) {
372
+ return defineName(
373
+ name,
374
+ () => Fail`[inquisitor] disallowed ${b(storeName)}.${b(name)}`,
375
+ );
376
+ } else {
377
+ throw Fail`[inquisitor] unknown ${b(storeName)} function ${b(name)}; time to update?`;
378
+ }
379
+ },
380
+ );
381
+ unseen.size === 0 ||
382
+ Fail`[inquisitor] ${b(storeName)} lacked ${q([...unseen])}; time to update?`;
383
+ // @ts-expect-error cast
384
+ return wrapped;
385
+ };
386
+ harden(wrapSubstore);
387
+
388
+ /**
389
+ * Make an overlay-like swing-store that buffers all mutations over a read-only
390
+ * database.
391
+ * If this ever needs substantial refactoring, consider pushing the
392
+ * functionality into swing-store itself.
393
+ *
394
+ * @param {string} dbPath to a swingstore.sqlite file
395
+ * @param {typeof wrapSubstore} wrapStore a function to replace swing-store sub-stores (kvStore/transcriptStore/etc.)
396
+ */
397
+ export const makeSwingStoreOverlay = (dbPath, wrapStore = wrapSubstore) => {
398
+ /** @type {Array<[storeName: string, operation: string, ...args: unknown[]]>} */
399
+ const mutations = [];
400
+ const recordCall = (storeName, operation, ...details) =>
401
+ mutations.push([storeName, operation, ...details]);
402
+ const makeWrapHelpers = () => {
403
+ const modifiedVats = new Set();
404
+ return {
405
+ log: recordCall,
406
+ isClean: () => modifiedVats.size === 0,
407
+ isStale: vatID => modifiedVats.has(vatID),
408
+ markStale: vatID => modifiedVats.add(vatID),
409
+ };
410
+ };
411
+
412
+ const kvListeners = {
413
+ onPendingSet: (key, value) => recordCall('kvStore', 'set', key, value),
414
+ onPendingDelete: key => recordCall('kvStore', 'delete', key),
415
+ };
416
+ const swingStore = openSwingStore(dbPath, {
417
+ asFile: true,
418
+ readonly: true,
419
+ wrapKvStore: base => makeBufferedStorage(base, kvListeners).kvStore,
420
+ wrapTranscriptStore: transcriptStore => {
421
+ const wrapHelpers = makeWrapHelpers();
422
+ const pendingItemsByVat = new Map();
423
+ /** @type {ReturnType<import('@agoric/swing-store').makeTranscriptStore>} */
424
+ const transcriptStoreOverride = {
425
+ ...transcriptStore,
426
+ addItem: (vatID, item) => {
427
+ recordCall('transcriptStore', 'addItem', vatID, item);
428
+ if (wrapHelpers.isStale(vatID)) return;
429
+ const pendingItems = pendingItemsByVat.get(vatID) || [];
430
+ const { startPos, endPos, hash, incarnation } =
431
+ pendingItems.at(-1) || transcriptStore.getCurrentSpanBounds(vatID);
432
+ pendingItems.push({
433
+ item,
434
+ startPos,
435
+ endPos: endPos + 1,
436
+ hash: hash && '<unknown>',
437
+ incarnation,
438
+ });
439
+ pendingItemsByVat.set(vatID, pendingItems);
440
+ },
441
+ getCurrentSpanBounds: vatID => {
442
+ const pendingItems = pendingItemsByVat.get(vatID) || [];
443
+ const { startPos, endPos, hash, incarnation } =
444
+ pendingItems.at(-1) || transcriptStore.getCurrentSpanBounds(vatID);
445
+ return { startPos, endPos, hash, incarnation };
446
+ },
447
+ readSpan: (vatID, startPos) => {
448
+ const reader = function* reader() {
449
+ try {
450
+ // Read from the base store.
451
+ yield* transcriptStore.readSpan(vatID, startPos);
452
+ } catch (_err) {}
453
+ // Read from the overlay, assuming that any transcripts of vatID
454
+ // are for the current span.
455
+ const pendingItems = pendingItemsByVat.get(vatID) || [];
456
+ for (const { item, startPos: itemStartPos } of pendingItems) {
457
+ if (startPos !== undefined && itemStartPos !== startPos) break;
458
+ yield item;
459
+ }
460
+ };
461
+ return reader();
462
+ },
463
+ };
464
+ return wrapStore('transcriptStore', transcriptStoreOverride, {
465
+ ...wrapHelpers,
466
+ logAndMark: [
467
+ 'initTranscript',
468
+ 'rolloverSpan',
469
+ 'rolloverIncarnation',
470
+ 'stopUsingTranscript',
471
+ ['deleteVatTranscripts', () => harden({ done: true, cleanups: 0 })],
472
+ ],
473
+ warnIfStale: ['addItem', 'getCurrentSpanBounds', 'readSpan'],
474
+ allowIfClean: [
475
+ ...storeExportAPI,
476
+ 'exportSpan',
477
+ 'dumpTranscripts',
478
+ 'readFullVatTranscript',
479
+ ],
480
+ disallow: [
481
+ 'importTranscriptSpanRecord',
482
+ 'populateTranscriptSpan',
483
+ 'assertComplete',
484
+ 'repairTranscriptSpanRecord',
485
+ ],
486
+ });
487
+ },
488
+ wrapSnapStore: snapStore => {
489
+ const wrapHelpers = makeWrapHelpers();
490
+ /** @type {ReturnType<import('@agoric/swing-store').makeSnapStore>} */
491
+ const snapStoreOverride = {
492
+ ...snapStore,
493
+ saveSnapshot: async (vatID, snapPos, dataStream) => {
494
+ const entryPrefix = ['snapStore', 'saveSnapshot', vatID, snapPos];
495
+ wrapHelpers.markStale(vatID);
496
+ await null;
497
+ let size = 0;
498
+ try {
499
+ for await (const chunk of dataStream) size += chunk.length;
500
+ recordCall(...entryPrefix, `<${size} bytes>`);
501
+ } catch (err) {
502
+ recordCall(...entryPrefix, `<error after ${size} bytes>`);
503
+ throw err;
504
+ }
505
+ return /** @type {import('@agoric/swing-store').SnapshotResult} */ (
506
+ harden({ uncompressedSize: size })
507
+ );
508
+ },
509
+ };
510
+ return wrapStore('snapStore', snapStoreOverride, {
511
+ ...wrapHelpers,
512
+ allow: ['saveSnapshot'],
513
+ logAndMark: [
514
+ ['deleteVatSnapshots', () => harden({ done: true, cleanups: 0 })],
515
+ 'stopUsingLastSnapshot',
516
+ ],
517
+ warnIfStale: ['loadSnapshot', 'getSnapshotInfo', 'hasHash'],
518
+ allowIfClean: [
519
+ ...storeExportAPI,
520
+ 'exportSnapshot',
521
+ 'listAllSnapshots',
522
+ 'dumpSnapshots',
523
+ ],
524
+ disallow: [
525
+ 'deleteAllUnusedSnapshots',
526
+ 'importSnapshotRecord',
527
+ 'populateSnapshot',
528
+ 'assertComplete',
529
+ 'repairSnapshotRecord',
530
+ 'deleteSnapshotByHash',
531
+ ],
532
+ });
533
+ },
534
+ wrapBundleStore: bundleStore => {
535
+ const overlayDB = sqlite3(':memory:');
536
+ const overlay = makeBundleStore(overlayDB, noop, noop);
537
+ let modified = false;
538
+ const onNewBundle = (operation, key, ...details) => {
539
+ modified = true;
540
+ recordCall('bundleStore', operation, key, ...details);
541
+ const bundleID = bundleIDFromName(key);
542
+ !bundleStore.hasBundle(bundleID) ||
543
+ Fail`base bundleStore already has ${bundleID}`;
544
+ };
545
+ /** @type {ReturnType<import('@agoric/swing-store').makeBundleStore>} */
546
+ const bundleStoreOverride = {
547
+ ...bundleStore,
548
+ // writes
549
+ importBundleRecord: (key, value) => {
550
+ onNewBundle('importBundleRecord', key, value);
551
+ return overlay.importBundleRecord(key, value);
552
+ },
553
+ importBundle: async (name, dataProvider) => {
554
+ const data = await dataProvider();
555
+ onNewBundle('importBundle', name, `<${data.length} bytes>`);
556
+ return overlay.importBundle(name, () => Promise.resolve(data));
557
+ },
558
+ addBundle: (bundleID, bundle) => {
559
+ onNewBundle('addBundle', `bundle.${bundleID}`, bundle.moduleFormat);
560
+ return overlay.addBundle(bundleID, bundle);
561
+ },
562
+ deleteBundle: bundleID => {
563
+ modified = true;
564
+ recordCall('bundleStore', 'deleteBundle', bundleID);
565
+ if (overlay.hasBundle(bundleID)) overlay.deleteBundle(bundleID);
566
+ },
567
+ // reads
568
+ hasBundle: bundleID =>
569
+ overlay.hasBundle(bundleID) || bundleStore.hasBundle(bundleID),
570
+ getBundle: bundleID => {
571
+ if (overlay.hasBundle(bundleID)) return overlay.getBundle(bundleID);
572
+ return bundleStore.getBundle(bundleID);
573
+ },
574
+ };
575
+ return wrapStore('bundleStore', bundleStoreOverride, {
576
+ log: recordCall,
577
+ isClean: () => !modified,
578
+ allow: [
579
+ 'importBundleRecord',
580
+ 'importBundle',
581
+ 'addBundle',
582
+ 'hasBundle',
583
+ 'getBundle',
584
+ 'deleteBundle',
585
+ ],
586
+ allowIfClean: [
587
+ ...storeExportAPI,
588
+ 'exportBundle',
589
+ 'getBundleIDs',
590
+ 'dumpBundles',
591
+ ],
592
+ disallow: ['assertComplete', 'repairBundleRecord'],
593
+ });
594
+ },
595
+ });
596
+
597
+ return { swingStore, mutations };
598
+ };
599
+ harden(makeSwingStoreOverlay);
600
+
601
+ /**
602
+ * Load a swing-store database for either REPL or scripted interactions.
603
+ *
604
+ * @param {[swingstoreDbPath: string]} argv
605
+ * @param {{ interactive?: boolean, historyFile?: string }} [options]
606
+ * @param {{ console?: typeof globalThis.console, process?: typeof globalThis.process }} [powers]
607
+ */
608
+ const main = async (argv, options = {}, powers = {}) => {
609
+ const { interactive, historyFile } = options;
610
+ const { console = globalThis.console, process = globalThis.process } = powers;
611
+ const { env } = process;
612
+ const maxVatsOnline = parseNumber(env.INQUISITOR_MAX_VATS_ONLINE || '3');
613
+
614
+ const { swingStore, mutations } = makeSwingStoreOverlay(argv[0]);
615
+ const { db, kvStore } = swingStore.internal;
616
+ const fakeStorageKit = makeFakeStorageKit('');
617
+ const { toStorage: handleVstorage } = fakeStorageKit;
618
+ const receiveBridgeSend = (destPort, msg) => {
619
+ console.log('[bridge] received', msg);
620
+ switch (destPort) {
621
+ case BridgeId.STORAGE: {
622
+ return handleVstorage(msg);
623
+ }
624
+ default:
625
+ Fail`[inquisitor] bridge port ${q(destPort)} not implemented for message ${msg}`;
626
+ }
627
+ };
628
+ const config = {
629
+ swingsetConfig: { maxVatsOnline },
630
+ swingStore,
631
+ /** @type {Partial<SwingSetConfig>} */
632
+ configOverrides: {
633
+ // Default to XS workers with no GC or snapshots.
634
+ defaultManagerType: 'xsnap',
635
+ defaultReapGCKrefs: 'never',
636
+ defaultReapInterval: 'never',
637
+ snapshotInterval: Number.MAX_VALUE,
638
+ },
639
+ fixupInitMessage: msg => ({
640
+ ...msg,
641
+ blockHeight: Number(swingStore.hostStorage.kvStore.get('host.height')),
642
+ blockTime: Math.floor(Date.now() / 1000 - 60),
643
+ // Default to no cleanup for terminated vats.
644
+ params: {
645
+ ...DEFAULT_SIM_SWINGSET_PARAMS,
646
+ ...msg.params,
647
+ vat_cleanup_budget: makeVatCleanupBudgetFromKeywords({ Default: 0 }),
648
+ },
649
+ }),
650
+ };
651
+ const testKit = await makeCosmicSwingsetTestKit(receiveBridgeSend, config);
652
+
653
+ const {
654
+ EV,
655
+ controller,
656
+ shutdown,
657
+ getLastBlockInfo,
658
+ pushQueueRecord,
659
+ pushCoreEval,
660
+ runNextBlock,
661
+ } = testKit;
662
+ const helpers = makeHelpers({ db, EV });
663
+ const endowments = {
664
+ // Raw access to overlay data.
665
+ ...{ kvStore: provideEnhancedKVStore(kvStore), swingStore },
666
+ // Block interactions
667
+ ...{ getLastBlockInfo, pushQueueRecord, pushCoreEval, runNextBlock },
668
+ // Vat interactions.
669
+ ...{ EV, controller, krefOf, kser, kslot, kunser },
670
+ // Inquisitor API.
671
+ ...{ mutations, ...helpers },
672
+ };
673
+ const contextDescriptors = objectMap(
674
+ { console, endowments, ...endowments, shutdown },
675
+ (value, name) => {
676
+ // For final cleanup, `shutdown` must be preserved.
677
+ if (name === 'shutdown') {
678
+ return { ...dataProp, value, writable: false, configurable: false };
679
+ }
680
+ return { ...dataProp, value };
681
+ },
682
+ );
683
+
684
+ if (!interactive) {
685
+ Object.defineProperties(globalThis, contextDescriptors);
686
+ return;
687
+ }
688
+
689
+ const truthyKeys = obj =>
690
+ Object.entries(obj).flatMap(([key, value]) => (value ? [key] : []));
691
+ console.warn('endowments:', ...truthyKeys(endowments));
692
+ console.warn('endowments.stable:', ...truthyKeys(endowments.stable));
693
+ const replServer = repl.start({
694
+ useGlobal: true,
695
+ // @ts-expect-error TS2322 REPLWriter really is allowed to return an Error
696
+ writer: value => {
697
+ if (value instanceof Error) {
698
+ // Use the SES console.
699
+ console.error(value);
700
+ return Object.defineProperty(Error(value.message), 'name', {
701
+ value: value.name,
702
+ });
703
+ }
704
+ return inspect(value, { colors: useColors, depth: inspectDepth });
705
+ },
706
+ });
707
+ if (historyFile) replServer.setupHistory(historyFile, _err => {});
708
+ Object.defineProperties(replServer.context, contextDescriptors);
709
+ const cleanup = () => shutdown().catch(noop);
710
+ replServer.on('exit', cleanup);
711
+ process.on('beforeExit', cleanup);
712
+ };
713
+
714
+ // Check for CLI invocation.
715
+ const isImport =
716
+ fs.realpathSync(process.argv[1]) !== fileURLToPath(import.meta.url);
717
+ const isCLIEntryPoint = !isImport && !process.send && isMainThread !== false;
718
+ const interactive = process.stdin.isTTY && !process.env.INQUISITOR_NO_REPL;
719
+ if (isCLIEntryPoint && !interactive) {
720
+ // When directly invoked with non-interactive stdin, defer to a child process
721
+ // that will read stdin as module statements in the global environment with
722
+ // `EV`/`controller`/`kvStore`/etc.
723
+ const args = [
724
+ '--input-type=module',
725
+ ...['--import', process.argv[1]],
726
+ '',
727
+ ...process.argv.slice(2),
728
+ ];
729
+ const child = spawn(process.argv[0], args, {
730
+ env: { ...process.env, INQUISITOR_NO_REPL: '1' },
731
+ stdio: ['pipe', 'inherit', 'inherit', 'ipc'],
732
+ });
733
+ const { promise: childDoneP, resolve: finishChild } = makePromiseKit();
734
+ child.on('error', error => setImmediate(finishChild, { error }));
735
+ child.on('exit', (code, signal) => finishChild({ code, signal }));
736
+ void childDoneP.then(resolveImmediate).then(async result => {
737
+ await null;
738
+ if (result?.signal) {
739
+ process.kill(process.pid, result.signal);
740
+ await delay(100);
741
+ }
742
+ if (typeof result?.code === 'number') process.exit(result.code);
743
+ const { error } = result;
744
+ console.error(error);
745
+ process.exit(error.code || 1);
746
+ });
747
+ const childInput = child.stdin;
748
+ if (!childInput) throw Fail`[inquisitor] child must have stdin`;
749
+ process.stdin.pipe(childInput, { end: false });
750
+ process.stdin.on('end', () => {
751
+ const cleanup = `\n; try { await shutdown(); } catch (_err) {}`;
752
+ stream.Readable.from([cleanup]).pipe(childInput);
753
+ });
754
+ } else if (isCLIEntryPoint || process.env.INQUISITOR_NO_REPL) {
755
+ // When directly invoked with interactive stdin OR as a worker above, parse
756
+ // CLI arguments and use `main` to setup the environment for either a REPL or
757
+ // evaluating stdin as module statements (respectively).
758
+ const homedir = os.homedir();
759
+ const defaultHistFile = pathlib.join(
760
+ homedir,
761
+ '.agoric_inquisitor_repl_history',
762
+ );
763
+ /** @typedef {{type: 'string' | 'boolean', short?: string, multiple?: boolean, default?: string | boolean | string[] | boolean[]}} ParseArgsOptionConfig */
764
+ /** @type {Record<string, ParseArgsOptionConfig>} */
765
+ const cliOptions = {
766
+ help: { type: 'boolean' },
767
+ 'history-file': {
768
+ type: 'string',
769
+ default: defaultHistFile,
770
+ },
771
+ };
772
+ const { values: options, positionals: args } = parseArgs({
773
+ options: cliOptions,
774
+ allowPositionals: true,
775
+ });
776
+ try {
777
+ if (options.help) throw Error();
778
+ args.length >= 1 || Fail`missing swingstore.sqlite`;
779
+ args.length === 1 || Fail`extra arguments`;
780
+ } catch (err) {
781
+ const log = options.help ? console.log : console.error;
782
+ if (!options.help) log(`Error: ${err.message}`);
783
+ const self = pathlib.relative(process.cwd(), process.argv[1]);
784
+ log(`Usage: ${self} swingstore.sqlite \\
785
+ [--history-file PATH (default ${cliOptions['history-file'].default})]
786
+
787
+ Loads an ephemeral environment in which one or more vats may be probed
788
+ via \`EV\`/\`controller\`/\`kvStore\`/\`mutations\`/etc. without persisting changes.
789
+ May be used interactively, or as a recipient of piped commands, or as a module.
790
+ Example commands:
791
+ * stable.db.prepare("SELECT name FROM sqlite_schema WHERE type='table'").pluck().all();
792
+ * stable.db.pragma("table_info(transcriptSpans)");
793
+ * [vatAdminNodeRow] = stable.db.kvGlob('v2.vs.*', '*v100*');
794
+ * stable.getRefs('o+10', 'v1');
795
+ * board = await EV.vat('bootstrap').consumeItem('board');
796
+ * obj = await EV(board).getValue('board02963');
797
+ * await runCoreEval(\`async powers => {
798
+ const ref = await E.get(powers.consume.auctioneerKit).governorAdminFacet;
799
+ console.log(ref);
800
+ powers.produce.ref.resolve(ref);
801
+ }\`);
802
+ * (await EV.vat('bootstrap').consumeItem('ref')).getKref()
803
+
804
+ ENVIRONMENT VARIABLES
805
+ CONSOLE_INSPECT_DEPTH
806
+ The number of times to recurse while formatting an object (default 6).
807
+ INQUISITOR_MAX_VATS_ONLINE
808
+ The maximum number of vats to have in memory at any given time (default 3).`);
809
+ process.exit(64);
810
+ }
811
+ const camelizedOptions = Object.fromEntries(
812
+ Object.entries(options).map(([name, value]) => [
813
+ name.replaceAll(/-([a-z])/g, (_, letter) => `${letter.toUpperCase()}`),
814
+ value,
815
+ ]),
816
+ );
817
+ // eslint-disable-next-line @jessie.js/safe-await-separator
818
+ await main(/** @type {[string]} */ (args), {
819
+ ...camelizedOptions,
820
+ interactive,
821
+ }).catch(err => {
822
+ console.error(err);
823
+ process.exit(err.code || 1);
824
+ });
825
+ }
package/tools/test-kit.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  } from '@agoric/internal/src/action-types.js';
14
14
  import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js';
15
15
  import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
16
+ import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js';
16
17
  import { initSwingStore } from '@agoric/swing-store';
17
-
18
18
  import {
19
19
  extractPortNums,
20
20
  makeLaunchChain,
@@ -23,6 +23,7 @@ import {
23
23
  import { DEFAULT_SIM_SWINGSET_PARAMS } from '../src/sim-params.js';
24
24
  import { makeQueue } from '../src/helpers/make-queue.js';
25
25
 
26
+ /** @import {EReturn} from '@endo/far'; */
26
27
  /** @import { BlockInfo, InitMsg } from '@agoric/internal/src/chain-utils.js' */
27
28
  /** @import { ManagerType, SwingSetConfig } from '@agoric/swingset-vat' */
28
29
  /** @import { InboundQueue } from '../src/launch-chain.js'; */
@@ -252,13 +253,25 @@ export const makeCosmicSwingsetTestKit = async (
252
253
  env,
253
254
  fs,
254
255
  path: nativePath,
255
- testingOverrides: { debugName, slogSender, swingStore, vatconfig: config },
256
+ testingOverrides: {
257
+ debugName,
258
+ slogSender,
259
+ swingStore,
260
+ vatconfig: config,
261
+ withInternals: true,
262
+ },
256
263
  });
257
264
  const launchResult = await launchChain({
258
265
  ...initMessage,
259
266
  resolvedConfig: swingsetConfig,
260
267
  });
261
- const { blockingSend, shutdown: shutdownKernel } = launchResult;
268
+ const {
269
+ blockingSend,
270
+ shutdown: shutdownKernel,
271
+ internals,
272
+ } = /** @type {EReturn<import('../src/launch-chain.js').launchAndShareInternals>} */ (
273
+ launchResult
274
+ );
262
275
  /** @type {(options?: { kernelOnly?: boolean }) => Promise<void>} */
263
276
  const shutdown = async ({ kernelOnly = false } = {}) => {
264
277
  await shutdownKernel();
@@ -266,6 +279,8 @@ export const makeCosmicSwingsetTestKit = async (
266
279
  await hostStorage.close();
267
280
  await cleanupDB();
268
281
  };
282
+ const { controller, bridgeInbound, timer } = internals;
283
+ const { queueAndRun, EV } = makeRunUtils(controller);
269
284
 
270
285
  // Remember information about the current block, starting with the init
271
286
  // message.
@@ -388,6 +403,13 @@ export const makeCosmicSwingsetTestKit = async (
388
403
  shutdown,
389
404
  swingStore,
390
405
 
406
+ // Controller-oriented helpers.
407
+ controller,
408
+ bridgeInbound,
409
+ timer,
410
+ queueAndRun,
411
+ EV,
412
+
391
413
  // Functions specific to this kit.
392
414
  getLastBlockInfo,
393
415
  pushQueueRecord,
package/tsconfig.json CHANGED
@@ -9,5 +9,6 @@
9
9
  "src/**/*.js",
10
10
  "test/**/*.js",
11
11
  "*.cjs",
12
+ "tools",
12
13
  ],
13
14
  }