@ametyst/cli 0.3.6 → 0.3.7

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.
Files changed (2) hide show
  1. package/dist/index.js +167 -11
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -112026,6 +112026,7 @@ var init_core = __esm({
112026
112026
  var grant_keystore_exports = {};
112027
112027
  __export(grant_keystore_exports, {
112028
112028
  GRANT_ENTRY_FORMAT: () => GRANT_ENTRY_FORMAT,
112029
+ MissingGrantKeyError: () => MissingGrantKeyError,
112029
112030
  buildGrantVault: () => buildGrantVault,
112030
112031
  createGrantKeyIssuer: () => createGrantKeyIssuer,
112031
112032
  deleteGrantEntry: () => deleteGrantEntry,
@@ -112033,6 +112034,8 @@ __export(grant_keystore_exports, {
112033
112034
  generateGrantKey: () => generateGrantKey,
112034
112035
  grantEntryPath: () => grantEntryPath,
112035
112036
  grantsDir: () => grantsDir,
112037
+ hasGrantSigningKey: () => hasGrantSigningKey,
112038
+ isMissingGrantKeyError: () => isMissingGrantKeyError,
112036
112039
  listGrantAddresses: () => listGrantAddresses,
112037
112040
  listGrantEntries: () => listGrantEntries,
112038
112041
  missingGrantKeyMessage: () => missingGrantKeyMessage,
@@ -112180,6 +112183,19 @@ function deleteGrantEntry(address, dir) {
112180
112183
  function missingGrantKeyMessage(address) {
112181
112184
  return `no local key for grant address ${address} \u2014 this machine cannot sign for that grant. Run requestAccess again to mint a fresh session key and have your admin approve it.`;
112182
112185
  }
112186
+ function isMissingGrantKeyError(err) {
112187
+ return err instanceof MissingGrantKeyError;
112188
+ }
112189
+ function hasGrantSigningKey(params) {
112190
+ let address;
112191
+ try {
112192
+ address = normalizeGrantAddress(params.address);
112193
+ } catch {
112194
+ return false;
112195
+ }
112196
+ if (readGrantEntry(address, params.dir)) return true;
112197
+ return typeof params.vaultAddress === "string" && params.vaultAddress.toLowerCase() === address;
112198
+ }
112183
112199
  function resolveGrantSigningKey(params) {
112184
112200
  const address = normalizeGrantAddress(params.address);
112185
112201
  const entry = readGrantEntry(address, params.dir);
@@ -112190,7 +112206,7 @@ function resolveGrantSigningKey(params) {
112190
112206
  if (params.vaultJson && typeof vaultAddress === "string" && vaultAddress.toLowerCase() === address) {
112191
112207
  return readVaultPrivateKey(params.vaultJson, params.passphrase);
112192
112208
  }
112193
- throw new Error(missingGrantKeyMessage(address));
112209
+ throw new MissingGrantKeyError(address);
112194
112210
  }
112195
112211
  function createGrantKeyIssuer(options) {
112196
112212
  return {
@@ -112214,7 +112230,7 @@ function createGrantKeyIssuer(options) {
112214
112230
  }
112215
112231
  };
112216
112232
  }
112217
- var GRANT_ENTRY_FORMAT, ADDRESS_RE2;
112233
+ var GRANT_ENTRY_FORMAT, ADDRESS_RE2, MissingGrantKeyError;
112218
112234
  var init_grant_keystore = __esm({
112219
112235
  "src/wallet/grant-keystore.ts"() {
112220
112236
  "use strict";
@@ -112225,6 +112241,14 @@ var init_grant_keystore = __esm({
112225
112241
  init_plaintext_vault();
112226
112242
  GRANT_ENTRY_FORMAT = "grant-key-v1";
112227
112243
  ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
112244
+ MissingGrantKeyError = class extends Error {
112245
+ grantAddress;
112246
+ constructor(address) {
112247
+ super(missingGrantKeyMessage(address));
112248
+ this.name = "MissingGrantKeyError";
112249
+ this.grantAddress = address;
112250
+ }
112251
+ };
112228
112252
  }
112229
112253
  });
112230
112254
 
@@ -112412,7 +112436,7 @@ var init_version5 = __esm({
112412
112436
  "src/version.ts"() {
112413
112437
  "use strict";
112414
112438
  init_esm_shims();
112415
- CLI_VERSION = true ? "0.3.6" : "0.0.0-dev";
112439
+ CLI_VERSION = true ? "0.3.7" : "0.0.0-dev";
112416
112440
  }
112417
112441
  });
112418
112442
 
@@ -115738,6 +115762,56 @@ function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
115738
115762
  );
115739
115763
  }
115740
115764
 
115765
+ // src/mcp-server/signer-binding.ts
115766
+ init_esm_shims();
115767
+ var BINDING_DERIVED_KEYS = [
115768
+ "virtualWalletKernelAccountClient",
115769
+ "permissionPlugin",
115770
+ "kernelDomain",
115771
+ "policyPrecheckPassed"
115772
+ ];
115773
+ function nonEmpty(value2) {
115774
+ return typeof value2 === "string" && value2.trim() !== "" ? value2 : void 0;
115775
+ }
115776
+ function sameAddress(a, b) {
115777
+ return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
115778
+ }
115779
+ function signerBindingMoved(state, row) {
115780
+ const rowId = row?.id != null ? String(row.id) : void 0;
115781
+ const rowAddress = nonEmpty(row?.address);
115782
+ if (state.virtualWalletId && rowId && state.virtualWalletId !== rowId) return true;
115783
+ if (state.signerAddress && rowAddress && !sameAddress(state.signerAddress, rowAddress)) return true;
115784
+ return false;
115785
+ }
115786
+ function bindSignerToGrantRow(state, row) {
115787
+ const changed = signerBindingMoved(state, row);
115788
+ if (changed) {
115789
+ for (const key of BINDING_DERIVED_KEYS) delete state[key];
115790
+ }
115791
+ const rowAddress = nonEmpty(row?.address);
115792
+ if (rowAddress) state.signerAddress = rowAddress;
115793
+ const rowWalletAddress = nonEmpty(row?.walletAddress) ?? nonEmpty(row?.kernelAccountAddress);
115794
+ const rowPaymentManagerAddress = nonEmpty(row?.paymentManagerAddress);
115795
+ const rowPolicyId = row?.policyAssociated != null ? String(row.policyAssociated) : void 0;
115796
+ const rowId = row?.id != null ? String(row.id) : void 0;
115797
+ if (changed) {
115798
+ state.walletAddress = rowWalletAddress;
115799
+ state.paymentManagerAddress = rowPaymentManagerAddress;
115800
+ state.policyId = rowPolicyId;
115801
+ state.policyOnchainPermissions = nonEmpty(row?.policyOnchainPermissions);
115802
+ } else {
115803
+ state.walletAddress = rowWalletAddress ?? state.walletAddress;
115804
+ state.paymentManagerAddress = rowPaymentManagerAddress ?? state.paymentManagerAddress;
115805
+ state.policyId = rowPolicyId ?? state.policyId;
115806
+ state.policyOnchainPermissions = nonEmpty(row?.policyOnchainPermissions) ?? state.policyOnchainPermissions;
115807
+ }
115808
+ if (rowId) state.virtualWalletId = rowId;
115809
+ if (row?.policyName != null) state.policyName = String(row.policyName);
115810
+ if (row?.policyValidUntil != null) state.policyValidUntil = BigInt(row.policyValidUntil);
115811
+ state.authorizationStatus = "approved";
115812
+ return { changed, signerAddress: state.signerAddress };
115813
+ }
115814
+
115741
115815
  // src/mcp-server/start-session.ts
115742
115816
  init_esm_shims();
115743
115817
  init_dist();
@@ -116049,11 +116123,11 @@ function clearWalletScopedCredentials(target) {
116049
116123
  delete target[key];
116050
116124
  }
116051
116125
  }
116052
- function sameAddress(a, b) {
116126
+ function sameAddress2(a, b) {
116053
116127
  return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
116054
116128
  }
116055
116129
  function mergeStartSessionCredentials(target, patch, responseKey, options = {}) {
116056
- const walletChanged = patch.eoaAddress !== void 0 && !sameAddress(patch.eoaAddress, target.eoaAddress);
116130
+ const walletChanged = patch.eoaAddress !== void 0 && !sameAddress2(patch.eoaAddress, target.eoaAddress);
116057
116131
  if (walletChanged || options.grantChanged) {
116058
116132
  clearWalletScopedCredentials(target);
116059
116133
  }
@@ -121919,6 +121993,49 @@ function signingKeyForAddress(address, passphrase) {
121919
121993
  passphrase: passphrase ?? currentCredentials.passphrase
121920
121994
  });
121921
121995
  }
121996
+ async function refreshSignerBinding() {
121997
+ if (!currentCredentials.apiKey) return { changed: false, address: currentCredentials.signerAddress };
121998
+ let wallets;
121999
+ try {
122000
+ wallets = await fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true);
122001
+ } catch (err) {
122002
+ console.error(
122003
+ `\u26A0\uFE0F [signer-binding] could not re-read the grants \u2014 signing with the cached binding (${currentCredentials.signerAddress ?? "none"}): ${err instanceof Error ? err.message : String(err)}`
122004
+ );
122005
+ wallets = walletsCache || [];
122006
+ }
122007
+ const row = currentCredentials.pendingWalletId ? findCurrentApprovedWallet(wallets, ownedSigningAddresses(), currentCredentials.pendingWalletId) : selectNewestApprovedWalletAmong(
122008
+ wallets,
122009
+ (Array.isArray(wallets) ? wallets : []).map((w) => w?.address)
122010
+ );
122011
+ if (!row) return { changed: false, address: currentCredentials.signerAddress };
122012
+ const previousSigner = currentCredentials.signerAddress;
122013
+ const { changed, signerAddress } = bindSignerToGrantRow(currentCredentials, row);
122014
+ if (changed) {
122015
+ console.error(
122016
+ `\u{1F501} [signer-binding] the active grant moved \u2014 signer ${previousSigner ?? "none"} -> ${signerAddress ?? "none"} (virtual wallet ${currentCredentials.virtualWalletId}, policy ${currentCredentials.policyId ?? "unknown"}). Dropping the signer state derived from the old grant.`
122017
+ );
122018
+ }
122019
+ return { changed, address: signerAddress };
122020
+ }
122021
+ async function resolveSignerForMaterialization(passphrase) {
122022
+ await refreshSignerBinding();
122023
+ const address = currentCredentials.signerAddress || currentCredentials.eoaAddress || "";
122024
+ if (!address) {
122025
+ throw new Error(
122026
+ "no signer address for this session \u2014 no approved grant and no install wallet. Run requestAccess first."
122027
+ );
122028
+ }
122029
+ return { address, privateKey: signingKeyForAddress(address, passphrase) };
122030
+ }
122031
+ function hasSigningKeyForAddress(address) {
122032
+ if (!address) return false;
122033
+ try {
122034
+ return hasGrantSigningKey({ address, vaultAddress: currentCredentials.eoaAddress });
122035
+ } catch {
122036
+ return false;
122037
+ }
122038
+ }
121922
122039
  var plaintextVaultMode = false;
121923
122040
  function refreshPlaintextVaultMode() {
121924
122041
  const vault = currentCredentials.walletKeystoreJson;
@@ -122983,12 +123100,15 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
122983
123100
  const wallets = walletsCache || [];
122984
123101
  const approvedWallet = findCurrentApprovedWallet(
122985
123102
  wallets,
122986
- eoaAddress,
123103
+ ownedSigningAddresses(),
122987
123104
  currentCredentials.pendingWalletId
122988
123105
  );
122989
123106
  if (approvedWallet) {
122990
123107
  console.error("\u2705 Found approved wallet from backend \u2014 authorization data stored");
122991
123108
  currentCredentials.authorizationStatus = "approved";
123109
+ if (typeof approvedWallet.address === "string" && approvedWallet.address.trim() !== "") {
123110
+ currentCredentials.signerAddress = approvedWallet.address;
123111
+ }
122992
123112
  currentCredentials.virtualWalletStatus = approvedWallet.status;
122993
123113
  currentCredentials.walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
122994
123114
  currentCredentials.virtualWalletId = String(approvedWallet.id);
@@ -123035,10 +123155,9 @@ async function createKernelClientFromVault() {
123035
123155
  throw new Error("No passphrase or keystore available");
123036
123156
  }
123037
123157
  if (!cliConfig) throw new Error("CLI config not initialized");
123038
- let privateKey = signingKeyForAddress(
123039
- currentCredentials.signerAddress || currentCredentials.eoaAddress || ""
123040
- );
123041
- console.error(`${ts()} Wallet key loaded`);
123158
+ const signer = await resolveSignerForMaterialization();
123159
+ let privateKey = signer.privateKey;
123160
+ console.error(`${ts()} Wallet key loaded \u2014 signing as ${signer.address}`);
123042
123161
  try {
123043
123162
  const { virtualWalletsManagers: virtualWalletsManagers2, financialAccounts: financialAccounts2 } = await getSDK();
123044
123163
  console.error(`${ts()} Fetching wallet data by API key...`);
@@ -123799,7 +123918,7 @@ server.tool(
123799
123918
  server.tool(
123800
123919
  {
123801
123920
  name: "getWalletStatus",
123802
- description: "Get wallet status: auth, balance, policy, allowlist, WHICH GRANT AND KEY THIS SESSION SIGNS WITH, WHICH WORKSPACE this server is acting in, and the last 10 transactions (amount, merchant, timestamp, status). `signerAddress` is the address of the approved grant this session signs with \u2014 since every access request now mints its OWN ephemeral session key, it is NOT the same as `eoaAddress` (the install wallet) except for a grant approved before rotation; `activeVirtualWalletId` / `activePolicyId` name that grant's row. `workspace` is `{companyName, employeeName}` \u2014 the Ametyst workspace every task, memory document and payment from this server lands in; both fields are null when the profile could not be read, which means UNKNOWN, never 'no workspace'. A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
123921
+ description: "Get wallet status: auth, balance, policy, allowlist, WHICH GRANT AND KEY THIS SESSION SIGNS WITH, WHICH WORKSPACE this server is acting in, and the last 10 transactions (amount, merchant, timestamp, status). `signerAddress` is the address of the approved grant this session signs with \u2014 since every access request now mints its OWN ephemeral session key, it is NOT the same as `eoaAddress` (the install wallet) except for a grant approved before rotation; `activeVirtualWalletId` / `activePolicyId` name that grant's row, and `signerKeyPresent` says whether this machine still holds the private key for `signerAddress` \u2014 `false` means no spend can be signed until `requestAccess` mints a fresh session key and an admin approves it. `workspace` is `{companyName, employeeName}` \u2014 the Ametyst workspace every task, memory document and payment from this server lands in; both fields are null when the profile could not be read, which means UNKNOWN, never 'no workspace'. A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
123803
123922
  inputs: []
123804
123923
  },
123805
123924
  async () => {
@@ -123834,6 +123953,9 @@ server.tool(
123834
123953
  const freshWallet = selectNewestApprovedWalletAmong(freshWallets, ownedSigningAddresses());
123835
123954
  const freshWalletIsActiveGrant = String(freshWallet?.id) === String(currentCredentials.virtualWalletId);
123836
123955
  if (freshWallet && freshWalletIsActiveGrant) {
123956
+ if (typeof freshWallet.address === "string" && freshWallet.address.trim() !== "") {
123957
+ currentCredentials.signerAddress = freshWallet.address;
123958
+ }
123837
123959
  if (freshWallet.policyAssociated != null) {
123838
123960
  currentCredentials.policyId = String(freshWallet.policyAssociated);
123839
123961
  }
@@ -123860,6 +123982,16 @@ server.tool(
123860
123982
  // "which key signed this" readable from a payload instead of from a
123861
123983
  // nonce-key autopsy — the same lesson as `activeVirtualWalletId` below.
123862
123984
  signerAddress: currentCredentials.signerAddress || currentCredentials.eoaAddress || null,
123985
+ // CAN THIS MACHINE ACTUALLY SIGN FOR THAT GRANT? A rotated grant is
123986
+ // signable only from the per-grant keystore entry minted with it, and a
123987
+ // machine that lost it (a re-install, a restored home, a grant approved
123988
+ // on another laptop) is bound to a signer it cannot produce. Presence
123989
+ // only — no decryption, so an encrypted vault with no cached passphrase
123990
+ // still reports `true` here and bounces at spend time on the passphrase,
123991
+ // which is a different and recoverable problem.
123992
+ signerKeyPresent: hasSigningKeyForAddress(
123993
+ currentCredentials.signerAddress || currentCredentials.eoaAddress
123994
+ ),
123863
123995
  walletAddress: currentCredentials.walletAddress || null,
123864
123996
  kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient,
123865
123997
  // WHICH GRANT THIS SESSION SIGNS WITH. The 2026-09-01 incident — three
@@ -125336,11 +125468,13 @@ server.tool(
125336
125468
  };
125337
125469
  }
125338
125470
  touchWalletActivity();
125471
+ let missingSignerKey;
125339
125472
  if (!currentCredentials.virtualWalletKernelAccountClient && currentCredentials.authorizationStatus === "approved" && isWalletUnlocked()) {
125340
125473
  try {
125341
125474
  console.error("\u{1F504} [spend] Auto-creating kernel client...");
125342
125475
  await createKernelClientFromVault();
125343
125476
  } catch (kcErr) {
125477
+ if (isMissingGrantKeyError(kcErr)) missingSignerKey = kcErr;
125344
125478
  console.error("\u274C [spend] Auto kernel client creation failed:", kcErr instanceof Error ? kcErr.message : String(kcErr));
125345
125479
  }
125346
125480
  }
@@ -125396,6 +125530,28 @@ server.tool(
125396
125530
  const apiKey = currentCredentials.apiKey;
125397
125531
  if (!kernelClient || !apiKey) {
125398
125532
  console.error("\u26A0\uFE0F [spend] Missing kernel client or API key in state");
125533
+ if (missingSignerKey) {
125534
+ logSpendTelemetryEarlyErr();
125535
+ return {
125536
+ content: [
125537
+ {
125538
+ type: "text",
125539
+ text: JSON.stringify({
125540
+ success: false,
125541
+ ...disclosePayment(),
125542
+ // PRE-PAYMENT
125543
+ error: "signer_key_missing",
125544
+ detail: missingSignerKey.message,
125545
+ guidance: {
125546
+ say_to_user: "This machine can't sign for your approved access any more \u2014 the session key it was issued to isn't here. I need to request access again so a fresh key can be approved.",
125547
+ next_action: "Call getAvailablePolicies(), let the user pick a policy, then call requestAccess with it. Once the admin approves the new request, retry this spend.",
125548
+ stop: true
125549
+ }
125550
+ })
125551
+ }
125552
+ ]
125553
+ };
125554
+ }
125399
125555
  if (currentCredentials.authorizationStatus === "pending") {
125400
125556
  logSpendTelemetryEarlyErr();
125401
125557
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametyst/cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "description": "Ametyst CLI — embedded MCP server, wallet ops, agent payments",
6
6
  "type": "module",
@@ -90,10 +90,10 @@
90
90
  "esbuild@>=0.27.3 <0.28.1": "0.28.1"
91
91
  },
92
92
  "optionalDependencies": {
93
- "@ametyst/cli-darwin-arm64": "0.3.6",
94
- "@ametyst/cli-darwin-x64": "0.3.6",
95
- "@ametyst/cli-linux-x64-gnu": "0.3.6",
96
- "@ametyst/cli-win32-x64-msvc": "0.3.6"
93
+ "@ametyst/cli-darwin-arm64": "0.3.7",
94
+ "@ametyst/cli-darwin-x64": "0.3.7",
95
+ "@ametyst/cli-linux-x64-gnu": "0.3.7",
96
+ "@ametyst/cli-win32-x64-msvc": "0.3.7"
97
97
  },
98
98
  "scripts": {
99
99
  "build:native": "cd native && cargo build --release && napi build --release",