@ametyst/cli 0.3.4 → 0.3.6

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 +671 -220
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -106446,7 +106446,12 @@ function loadConfig() {
106446
106446
  // pbi-wallet-3 — projected, or the cached stance would survive exactly one
106447
106447
  // process and then vanish (the whitelist warning above).
106448
106448
  ...typeof p.requireWalletPassphrase === "boolean" ? { requireWalletPassphrase: p.requireWalletPassphrase } : {},
106449
- ...typeof p.requireWalletPassphraseObservedAt === "string" ? { requireWalletPassphraseObservedAt: p.requireWalletPassphraseObservedAt } : {}
106449
+ ...typeof p.requireWalletPassphraseObservedAt === "string" ? { requireWalletPassphraseObservedAt: p.requireWalletPassphraseObservedAt } : {},
106450
+ // The workspace binding — projected, or a switch could never be detected:
106451
+ // the key would survive exactly one process and then vanish (the whitelist
106452
+ // warning above), making every login look like a first binding.
106453
+ ...typeof p.workspaceId === "string" ? { workspaceId: p.workspaceId } : {},
106454
+ ...typeof p.workspaceIdObservedAt === "string" ? { workspaceIdObservedAt: p.workspaceIdObservedAt } : {}
106450
106455
  };
106451
106456
  } catch {
106452
106457
  return null;
@@ -111782,7 +111787,8 @@ __export(core_exports, {
111782
111787
  reencryptPlaintextVault: () => reencryptPlaintextVault,
111783
111788
  safeVaultErrorMessage: () => safeVaultErrorMessage,
111784
111789
  saveWallet: () => saveWallet,
111785
- wipeVault: () => wipeVault
111790
+ wipeVault: () => wipeVault,
111791
+ writeSecretFileAtomic: () => writeSecretFileAtomic
111786
111792
  });
111787
111793
  import {
111788
111794
  chmodSync as chmodSync2,
@@ -111873,8 +111879,11 @@ function writeAllSync(fd, buf) {
111873
111879
  }
111874
111880
  function writeVaultFileAtomic(contents) {
111875
111881
  ensureDirectories();
111876
- const dir = dirname5(VAULT_PATH);
111877
- const tmp = join5(dir, `.wallet.json.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
111882
+ writeSecretFileAtomic(VAULT_PATH, contents);
111883
+ }
111884
+ function writeSecretFileAtomic(path2, contents) {
111885
+ const dir = dirname5(path2);
111886
+ const tmp = join5(dir, `.${basename3(path2)}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
111878
111887
  let fd;
111879
111888
  try {
111880
111889
  fd = openSync(tmp, "wx", 384);
@@ -111883,7 +111892,7 @@ function writeVaultFileAtomic(contents) {
111883
111892
  fsyncSync(fd);
111884
111893
  closeSync(fd);
111885
111894
  fd = void 0;
111886
- renameSync(tmp, VAULT_PATH);
111895
+ renameSync(tmp, path2);
111887
111896
  fsyncDir(dir);
111888
111897
  } catch (e) {
111889
111898
  if (fd !== void 0) {
@@ -112013,6 +112022,212 @@ var init_core = __esm({
112013
112022
  }
112014
112023
  });
112015
112024
 
112025
+ // src/wallet/grant-keystore.ts
112026
+ var grant_keystore_exports = {};
112027
+ __export(grant_keystore_exports, {
112028
+ GRANT_ENTRY_FORMAT: () => GRANT_ENTRY_FORMAT,
112029
+ buildGrantVault: () => buildGrantVault,
112030
+ createGrantKeyIssuer: () => createGrantKeyIssuer,
112031
+ deleteGrantEntry: () => deleteGrantEntry,
112032
+ ensureGrantsDir: () => ensureGrantsDir,
112033
+ generateGrantKey: () => generateGrantKey,
112034
+ grantEntryPath: () => grantEntryPath,
112035
+ grantsDir: () => grantsDir,
112036
+ listGrantAddresses: () => listGrantAddresses,
112037
+ listGrantEntries: () => listGrantEntries,
112038
+ missingGrantKeyMessage: () => missingGrantKeyMessage,
112039
+ normalizeGrantAddress: () => normalizeGrantAddress,
112040
+ parseGrantEntry: () => parseGrantEntry,
112041
+ persistGrantKey: () => persistGrantKey,
112042
+ readGrantEntry: () => readGrantEntry,
112043
+ recordGrantRowId: () => recordGrantRowId,
112044
+ resolveGrantSigningKey: () => resolveGrantSigningKey
112045
+ });
112046
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readdirSync as readdirSync4, unlinkSync as unlinkSync5 } from "fs";
112047
+ import { join as join6 } from "path";
112048
+ function grantsDir() {
112049
+ return join6(WALLETS_DIR, "grants");
112050
+ }
112051
+ function resolveDir(dir) {
112052
+ return dir ?? grantsDir();
112053
+ }
112054
+ function ensureGrantsDir(dir) {
112055
+ const target = resolveDir(dir);
112056
+ if (target === grantsDir()) ensureDirectories();
112057
+ if (!existsSync7(target)) mkdirSync6(target, { mode: 448, recursive: true });
112058
+ return target;
112059
+ }
112060
+ function normalizeGrantAddress(address) {
112061
+ if (typeof address !== "string" || !ADDRESS_RE2.test(address)) {
112062
+ throw new Error(`Not a 0x-hex EOA address: ${JSON.stringify(address)}`);
112063
+ }
112064
+ return address.toLowerCase();
112065
+ }
112066
+ function grantEntryPath(address, dir) {
112067
+ return join6(resolveDir(dir), `${normalizeGrantAddress(address)}.json`);
112068
+ }
112069
+ function generateGrantKey() {
112070
+ const derived = derivePlaintextWallet(generatePrivateKey());
112071
+ return { address: derived.address.toLowerCase(), privateKey: derived.privateKey };
112072
+ }
112073
+ function buildGrantVault(address, privateKey, options) {
112074
+ if (options.encrypted) {
112075
+ if (!options.passphrase) {
112076
+ throw new Error(
112077
+ "Refusing to store a session key unencrypted: this machine's wallet is passphrase-protected but no passphrase is in memory. Call start_session({ passphrase }) first."
112078
+ );
112079
+ }
112080
+ return JSON.parse(importWallet(privateKey, options.passphrase));
112081
+ }
112082
+ return JSON.parse(serializePlaintextVault(address, privateKey));
112083
+ }
112084
+ function persistGrantKey(params, dir) {
112085
+ const address = normalizeGrantAddress(params.address);
112086
+ const target = ensureGrantsDir(dir);
112087
+ const entry = {
112088
+ format: GRANT_ENTRY_FORMAT,
112089
+ address,
112090
+ createdAt: params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
112091
+ workspaceId: params.workspaceId ?? null,
112092
+ policyId: params.policyId ?? null,
112093
+ backendWalletRowId: params.backendWalletRowId ?? null,
112094
+ vault: buildGrantVault(address, params.privateKey, {
112095
+ encrypted: params.encrypted,
112096
+ passphrase: params.passphrase
112097
+ })
112098
+ };
112099
+ writeSecretFileAtomic(join6(target, `${address}.json`), JSON.stringify(entry, null, 2));
112100
+ return entry;
112101
+ }
112102
+ function parseGrantEntry(json) {
112103
+ let parsed;
112104
+ try {
112105
+ parsed = JSON.parse(json);
112106
+ } catch {
112107
+ return null;
112108
+ }
112109
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
112110
+ const p = parsed;
112111
+ if (p.format !== GRANT_ENTRY_FORMAT) return null;
112112
+ if (typeof p.address !== "string" || !ADDRESS_RE2.test(p.address)) return null;
112113
+ if (p.vault === null || typeof p.vault !== "object" || Array.isArray(p.vault)) return null;
112114
+ return {
112115
+ format: GRANT_ENTRY_FORMAT,
112116
+ address: p.address.toLowerCase(),
112117
+ createdAt: typeof p.createdAt === "string" ? p.createdAt : "",
112118
+ workspaceId: typeof p.workspaceId === "string" ? p.workspaceId : null,
112119
+ policyId: typeof p.policyId === "number" ? p.policyId : null,
112120
+ backendWalletRowId: typeof p.backendWalletRowId === "string" ? p.backendWalletRowId : null,
112121
+ vault: p.vault
112122
+ };
112123
+ }
112124
+ function readGrantEntry(address, dir) {
112125
+ let path2;
112126
+ try {
112127
+ path2 = grantEntryPath(address, dir);
112128
+ } catch {
112129
+ return null;
112130
+ }
112131
+ try {
112132
+ return parseGrantEntry(readFileSync7(path2, "utf-8"));
112133
+ } catch {
112134
+ return null;
112135
+ }
112136
+ }
112137
+ function listGrantEntries(dir) {
112138
+ const target = resolveDir(dir);
112139
+ let names;
112140
+ try {
112141
+ names = readdirSync4(target);
112142
+ } catch {
112143
+ return [];
112144
+ }
112145
+ const entries = [];
112146
+ for (const name of names) {
112147
+ if (!name.endsWith(".json")) continue;
112148
+ let raw;
112149
+ try {
112150
+ raw = readFileSync7(join6(target, name), "utf-8");
112151
+ } catch {
112152
+ continue;
112153
+ }
112154
+ const entry = parseGrantEntry(raw);
112155
+ if (entry) entries.push(entry);
112156
+ }
112157
+ return entries;
112158
+ }
112159
+ function listGrantAddresses(options = {}, dir) {
112160
+ const active = options.workspaceId ?? null;
112161
+ return listGrantEntries(dir).filter((e) => active === null || e.workspaceId === null || e.workspaceId === active).map((e) => e.address);
112162
+ }
112163
+ function recordGrantRowId(address, rowId, dir) {
112164
+ const entry = readGrantEntry(address, dir);
112165
+ if (!entry) return false;
112166
+ try {
112167
+ const next = { ...entry, backendWalletRowId: rowId ?? null };
112168
+ writeSecretFileAtomic(grantEntryPath(address, dir), JSON.stringify(next, null, 2));
112169
+ return true;
112170
+ } catch {
112171
+ return false;
112172
+ }
112173
+ }
112174
+ function deleteGrantEntry(address, dir) {
112175
+ try {
112176
+ unlinkSync5(grantEntryPath(address, dir));
112177
+ } catch {
112178
+ }
112179
+ }
112180
+ function missingGrantKeyMessage(address) {
112181
+ 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
+ }
112183
+ function resolveGrantSigningKey(params) {
112184
+ const address = normalizeGrantAddress(params.address);
112185
+ const entry = readGrantEntry(address, params.dir);
112186
+ if (entry) {
112187
+ return readVaultPrivateKey(JSON.stringify(entry.vault), params.passphrase);
112188
+ }
112189
+ const vaultAddress = params.vaultAddress;
112190
+ if (params.vaultJson && typeof vaultAddress === "string" && vaultAddress.toLowerCase() === address) {
112191
+ return readVaultPrivateKey(params.vaultJson, params.passphrase);
112192
+ }
112193
+ throw new Error(missingGrantKeyMessage(address));
112194
+ }
112195
+ function createGrantKeyIssuer(options) {
112196
+ return {
112197
+ issue({ policyId }) {
112198
+ const { address, privateKey } = generateGrantKey();
112199
+ persistGrantKey(
112200
+ {
112201
+ address,
112202
+ privateKey,
112203
+ encrypted: options.encrypted,
112204
+ passphrase: options.passphrase,
112205
+ workspaceId: options.workspaceId ?? null,
112206
+ policyId
112207
+ },
112208
+ options.dir
112209
+ );
112210
+ return address;
112211
+ },
112212
+ recordRow(address, rowId) {
112213
+ recordGrantRowId(address, rowId, options.dir);
112214
+ }
112215
+ };
112216
+ }
112217
+ var GRANT_ENTRY_FORMAT, ADDRESS_RE2;
112218
+ var init_grant_keystore = __esm({
112219
+ "src/wallet/grant-keystore.ts"() {
112220
+ "use strict";
112221
+ init_esm_shims();
112222
+ init_accounts();
112223
+ init_paths();
112224
+ init_core();
112225
+ init_plaintext_vault();
112226
+ GRANT_ENTRY_FORMAT = "grant-key-v1";
112227
+ ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
112228
+ }
112229
+ });
112230
+
112016
112231
  // src/commands/init-claim.ts
112017
112232
  var init_claim_exports = {};
112018
112233
  __export(init_claim_exports, {
@@ -112026,7 +112241,7 @@ __export(init_claim_exports, {
112026
112241
  vaultBackupPath: () => vaultBackupPath,
112027
112242
  vaultBackupSuffix: () => vaultBackupSuffix
112028
112243
  });
112029
- import { chmodSync as chmodSync3, constants as fsConstants, copyFileSync as copyFileSync3, existsSync as existsSync7 } from "fs";
112244
+ import { chmodSync as chmodSync3, constants as fsConstants, copyFileSync as copyFileSync3, existsSync as existsSync8 } from "fs";
112030
112245
  import { createDecipheriv as createDecipheriv2 } from "crypto";
112031
112246
  function claimErrorMessage(failure) {
112032
112247
  switch (failure.kind) {
@@ -112169,7 +112384,7 @@ function vaultBackupPath(vaultPath, at) {
112169
112384
  return `${vaultPath}.bak-${vaultBackupSuffix(at)}`;
112170
112385
  }
112171
112386
  function backupVaultFile(vaultPath, at = /* @__PURE__ */ new Date()) {
112172
- if (!existsSync7(vaultPath)) return null;
112387
+ if (!existsSync8(vaultPath)) return null;
112173
112388
  const target = vaultBackupPath(vaultPath, at);
112174
112389
  try {
112175
112390
  copyFileSync3(vaultPath, target, fsConstants.COPYFILE_EXCL);
@@ -112197,13 +112412,13 @@ var init_version5 = __esm({
112197
112412
  "src/version.ts"() {
112198
112413
  "use strict";
112199
112414
  init_esm_shims();
112200
- CLI_VERSION = true ? "0.3.4" : "0.0.0-dev";
112415
+ CLI_VERSION = true ? "0.3.6" : "0.0.0-dev";
112201
112416
  }
112202
112417
  });
112203
112418
 
112204
112419
  // src/connections/catalog.ts
112205
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
112206
- import { join as join6 } from "path";
112420
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
112421
+ import { join as join7 } from "path";
112207
112422
  function declaredVerbsHeader() {
112208
112423
  return SUPPORTED_HTTP_METHODS.join(",");
112209
112424
  }
@@ -112459,8 +112674,8 @@ function parseConnectorsResponseDetailed(body, onWarn = warnToStderr) {
112459
112674
  function readCachedCatalogStatus() {
112460
112675
  let raw;
112461
112676
  try {
112462
- if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112463
- raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112677
+ if (!existsSync9(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112678
+ raw = JSON.parse(readFileSync8(CONNECTOR_CACHE_PATH, "utf-8"));
112464
112679
  } catch (e) {
112465
112680
  return { state: "unreadable", reason: e instanceof Error ? e.message : String(e) };
112466
112681
  }
@@ -112496,8 +112711,8 @@ function readMirrorRowsFromDisk() {
112496
112711
  }
112497
112712
  function readMirrorContainer() {
112498
112713
  try {
112499
- if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112500
- const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112714
+ if (!existsSync9(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112715
+ const raw = JSON.parse(readFileSync8(CONNECTOR_CACHE_PATH, "utf-8"));
112501
112716
  if (!isRecord(raw)) {
112502
112717
  return { state: "unreadable", reason: "the mirror's top level is not an object" };
112503
112718
  }
@@ -112511,8 +112726,8 @@ function readMirrorContainer() {
112511
112726
  }
112512
112727
  function readMirrorFetchedAt() {
112513
112728
  try {
112514
- if (!existsSync8(CONNECTOR_CACHE_PATH)) return "";
112515
- const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112729
+ if (!existsSync9(CONNECTOR_CACHE_PATH)) return "";
112730
+ const raw = JSON.parse(readFileSync8(CONNECTOR_CACHE_PATH, "utf-8"));
112516
112731
  if (!isRecord(raw)) return "";
112517
112732
  return typeof raw.fetchedAt === "string" ? raw.fetchedAt : "";
112518
112733
  } catch {
@@ -112763,7 +112978,7 @@ var init_catalog = __esm({
112763
112978
  SOURCE_OPERATORS = ["provider", "third_party", "ametyst"];
112764
112979
  SOURCE_PREFERENCE_RULES = ["official-mcp"];
112765
112980
  CONNECTOR_FEED_PATH = "/api/v1/virtual-wallets/capabilities/connectors";
112766
- CONNECTOR_CACHE_PATH = join6(AMETYST_DIR, "connectors.json");
112981
+ CONNECTOR_CACHE_PATH = join7(AMETYST_DIR, "connectors.json");
112767
112982
  CatalogUnavailableError = class extends Error {
112768
112983
  constructor(reason, message) {
112769
112984
  super(message);
@@ -113501,15 +113716,15 @@ var init_call2 = __esm({
113501
113716
  });
113502
113717
 
113503
113718
  // src/connections/engine-store.ts
113504
- import { chmodSync as chmodSync4, closeSync as closeSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, openSync as openSync2 } from "fs";
113505
- import { dirname as dirname7, join as join16 } from "path";
113719
+ import { chmodSync as chmodSync4, closeSync as closeSync2, existsSync as existsSync12, mkdirSync as mkdirSync8, openSync as openSync2 } from "fs";
113720
+ import { dirname as dirname7, join as join17 } from "path";
113506
113721
  function connectionsDbPath() {
113507
- return join16(AMETYST_DIR, CONNECTIONS_DB_FILENAME);
113722
+ return join17(AMETYST_DIR, CONNECTIONS_DB_FILENAME);
113508
113723
  }
113509
113724
  function ensureOwnerOnlyFile(path2) {
113510
113725
  if (path2 === IN_MEMORY_DB) return;
113511
- mkdirSync7(dirname7(path2), { recursive: true, mode: DIR_MODE });
113512
- if (!existsSync11(path2)) closeSync2(openSync2(path2, "a", CONNECTIONS_DB_FILE_MODE));
113726
+ mkdirSync8(dirname7(path2), { recursive: true, mode: DIR_MODE });
113727
+ if (!existsSync12(path2)) closeSync2(openSync2(path2, "a", CONNECTIONS_DB_FILE_MODE));
113513
113728
  chmodSync4(path2, CONNECTIONS_DB_FILE_MODE);
113514
113729
  }
113515
113730
  function sqliteWasmDriver(db, raw) {
@@ -114718,6 +114933,44 @@ function hostNudges(rows) {
114718
114933
 
114719
114934
  // src/commands/login.ts
114720
114935
  init_resolve();
114936
+
114937
+ // src/config/workspace-binding.ts
114938
+ init_esm_shims();
114939
+ init_config();
114940
+ function workspaceKeyFrom(profile) {
114941
+ const name = profile?.companyName;
114942
+ if (typeof name !== "string") return null;
114943
+ const trimmed = name.trim().toLowerCase();
114944
+ return trimmed === "" ? null : trimmed;
114945
+ }
114946
+ function isWorkspaceSwitch(previous, next) {
114947
+ if (previous === null || next === null) return false;
114948
+ return previous !== next;
114949
+ }
114950
+ function readBoundWorkspace() {
114951
+ const config = loadConfig();
114952
+ const key = config?.workspaceId;
114953
+ return typeof key === "string" && key.trim() !== "" ? key : null;
114954
+ }
114955
+ function recordBoundWorkspace(key, observedAt) {
114956
+ const config = loadConfig();
114957
+ if (!config) return false;
114958
+ const { apiKey: _injected, apiKeySource: _src, ...persisted } = config;
114959
+ saveConfig({ ...persisted, workspaceId: key, workspaceIdObservedAt: observedAt });
114960
+ return true;
114961
+ }
114962
+ function workspaceSwitchNotice(previous, next) {
114963
+ return `Workspace changed: "${previous}" -> "${next}". Dropping the cached session and wallet binding from the previous workspace \u2014 its session keys are kept on disk (they stay signable if you switch back) but are no longer selected here.`;
114964
+ }
114965
+ function applyObservedWorkspace(profile, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
114966
+ const key = workspaceKeyFrom(profile);
114967
+ const previous = readBoundWorkspace();
114968
+ const switched = isWorkspaceSwitch(previous, key);
114969
+ if (key !== null && key !== previous) recordBoundWorkspace(key, observedAt);
114970
+ return { key, previous, switched };
114971
+ }
114972
+
114973
+ // src/commands/login.ts
114721
114974
  function lastObservedStanceLine(config) {
114722
114975
  const observed = config?.requireWalletPassphrase;
114723
114976
  if (typeof observed !== "boolean") {
@@ -114779,6 +115032,10 @@ async function loginCommand(options) {
114779
115032
  if (observedStance !== void 0) {
114780
115033
  setRequireWalletPassphrase(observedStance, (/* @__PURE__ */ new Date()).toISOString());
114781
115034
  }
115035
+ const workspace = applyObservedWorkspace(profile);
115036
+ if (workspace.switched && workspace.previous && workspace.key) {
115037
+ console.log(workspaceSwitchNotice(workspace.previous, workspace.key));
115038
+ }
114782
115039
  const stance = resolveWalletPassphraseStance(observedStance, config.requireWalletPassphrase);
114783
115040
  const who = profile ? `Logged in as ${profile.employeeName} @ ${profile.companyName ?? "(no company)"}.` : "Logged in.";
114784
115041
  console.log(who);
@@ -114926,8 +115183,9 @@ async function fetchAvailablePolicies(client, apiKey) {
114926
115183
  const response = await client.getPoliciesFromVirtualWalletsManager(apiKey);
114927
115184
  return normalizePoliciesResponse(response);
114928
115185
  }
114929
- async function submitAccessRequest(client, apiKey, eoaAddress, policyId) {
114930
- return client.createVirtualWallet(
115186
+ async function submitRotatedAccessRequest(client, apiKey, policyId, issuer) {
115187
+ const eoaAddress = await issuer.issue({ policyId });
115188
+ const result = await client.createVirtualWallet(
114931
115189
  apiKey,
114932
115190
  CLI_WALLET_NAME,
114933
115191
  CLI_WALLET_URL,
@@ -114935,6 +115193,11 @@ async function submitAccessRequest(client, apiKey, eoaAddress, policyId) {
114935
115193
  policyId,
114936
115194
  eoaAddress
114937
115195
  );
115196
+ try {
115197
+ issuer.recordRow?.(eoaAddress, result?.id ? String(result.id) : void 0);
115198
+ } catch {
115199
+ }
115200
+ return { ...result, eoaAddress };
114938
115201
  }
114939
115202
  function pickDefaultPolicyIndex(policies) {
114940
115203
  const i = policies.findIndex(
@@ -115133,6 +115396,7 @@ async function freshWalletRecovery(apiKey, options) {
115133
115396
  }
115134
115397
  }
115135
115398
  let address;
115399
+ let vaultPassphrase;
115136
115400
  if (options.noPassphrase) {
115137
115401
  const wallet = createPlaintextWallet();
115138
115402
  persistPlaintextVault(wallet.address, wallet.privateKey);
@@ -115153,6 +115417,7 @@ New wallet created. Address: ${address}`);
115153
115417
  process.exit(1);
115154
115418
  return false;
115155
115419
  }
115420
+ vaultPassphrase = passphrase;
115156
115421
  const keystoreJson = createWallet(passphrase);
115157
115422
  persistVault(keystoreJson);
115158
115423
  address = getWalletAddress(keystoreJson);
@@ -115201,8 +115466,15 @@ Create one in the Ametyst web app (admin panel \u2192 policies), then re-run 'am
115201
115466
  process.exit(1);
115202
115467
  return false;
115203
115468
  }
115469
+ let grantAddress;
115204
115470
  try {
115205
- await submitAccessRequest(client, apiKey, address, policyId);
115471
+ const { createGrantKeyIssuer: createGrantKeyIssuer2 } = await Promise.resolve().then(() => (init_grant_keystore(), grant_keystore_exports));
115472
+ const submitted = await submitRotatedAccessRequest(client, apiKey, policyId, createGrantKeyIssuer2({
115473
+ encrypted: !options.noPassphrase,
115474
+ passphrase: vaultPassphrase,
115475
+ workspaceId: readBoundWorkspace()
115476
+ }));
115477
+ grantAddress = submitted.eoaAddress;
115206
115478
  } catch (e) {
115207
115479
  const msg = e instanceof Error ? e.message : String(e);
115208
115480
  console.error(`
@@ -115214,8 +115486,9 @@ Could not submit the authorization request (${msg}).`);
115214
115486
  const policyName = typeof chosen.name === "string" && chosen.name ? chosen.name : `policy ${policyId}`;
115215
115487
  console.log(`
115216
115488
  Authorization request sent \u2014 PENDING approval.
115217
- Wallet: ${address}
115218
- Policy: ${policyName} (id ${policyId})
115489
+ Wallet: ${address}
115490
+ Session key: ${grantAddress}
115491
+ Policy: ${policyName} (id ${policyId})
115219
115492
 
115220
115493
  Next step: approve the request in the Ametyst web app admin panel (check your
115221
115494
  notifications there). Once it's approved, you're done \u2014 run 'ametyst status' to
@@ -115417,6 +115690,54 @@ var McpEventLogger = class {
115417
115690
  }
115418
115691
  };
115419
115692
 
115693
+ // src/mcp-server/newest-grant.ts
115694
+ init_esm_shims();
115695
+ function nonEmptyLowerCase(value2) {
115696
+ return typeof value2 === "string" && value2.trim() !== "" ? value2.toLowerCase() : null;
115697
+ }
115698
+ function isNewerGrant(candidate, incumbent) {
115699
+ const candidateId = Number(candidate?.id);
115700
+ const incumbentId = Number(incumbent?.id);
115701
+ const candidateIdUsable = Number.isFinite(candidateId);
115702
+ const incumbentIdUsable = Number.isFinite(incumbentId);
115703
+ if (candidateIdUsable && incumbentIdUsable && candidateId !== incumbentId) {
115704
+ return candidateId > incumbentId;
115705
+ }
115706
+ const candidateAt = Date.parse(typeof candidate?.createdAt === "string" ? candidate.createdAt : "");
115707
+ const incumbentAt = Date.parse(typeof incumbent?.createdAt === "string" ? incumbent.createdAt : "");
115708
+ if (Number.isFinite(candidateAt) && Number.isFinite(incumbentAt) && candidateAt !== incumbentAt) {
115709
+ return candidateAt > incumbentAt;
115710
+ }
115711
+ return candidateIdUsable && !incumbentIdUsable;
115712
+ }
115713
+ function selectNewestApprovedWalletAmong(wallets, addresses) {
115714
+ if (!Array.isArray(wallets)) return void 0;
115715
+ const candidates = /* @__PURE__ */ new Set();
115716
+ for (const address of addresses ?? []) {
115717
+ const normalized = nonEmptyLowerCase(address);
115718
+ if (normalized) candidates.add(normalized);
115719
+ }
115720
+ if (candidates.size === 0) return void 0;
115721
+ let newest;
115722
+ for (const wallet of wallets) {
115723
+ if (wallet?.status !== "approved") continue;
115724
+ const rowAddress = nonEmptyLowerCase(wallet?.address);
115725
+ if (!rowAddress || !candidates.has(rowAddress)) continue;
115726
+ if (newest === void 0 || isNewerGrant(wallet, newest)) newest = wallet;
115727
+ }
115728
+ return newest;
115729
+ }
115730
+ function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
115731
+ if (!Array.isArray(wallets)) return void 0;
115732
+ if (pendingWalletId) {
115733
+ return wallets.find((w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved");
115734
+ }
115735
+ return selectNewestApprovedWalletAmong(
115736
+ wallets,
115737
+ Array.isArray(eoaAddress) ? eoaAddress : eoaAddress === void 0 ? [] : [eoaAddress]
115738
+ );
115739
+ }
115740
+
115420
115741
  // src/mcp-server/start-session.ts
115421
115742
  init_esm_shims();
115422
115743
  init_dist();
@@ -115551,43 +115872,69 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115551
115872
  let virtualWalletId = ctx.virtualWalletId;
115552
115873
  let paymentManagerAddress = ctx.paymentManagerAddress;
115553
115874
  let policyId = ctx.policyId;
115554
- if (!walletAddress || !virtualWalletId) {
115555
- try {
115556
- const approvedWallet = wallets.find(
115557
- (w) => w.address?.toLowerCase() === ctx.eoaAddress?.toLowerCase() && w.status === "approved"
115558
- );
115559
- if (approvedWallet) {
115560
- walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
115561
- virtualWalletId = String(approvedWallet.id);
115562
- paymentManagerAddress = approvedWallet.paymentManagerAddress;
115563
- policyId = String(approvedWallet.policyAssociated);
115564
- credentials.walletAddress = walletAddress;
115565
- credentials.virtualWalletId = virtualWalletId;
115566
- credentials.paymentManagerAddress = paymentManagerAddress;
115567
- credentials.policyId = policyId;
115568
- credentials.authorizationStatus = "approved";
115569
- if (approvedWallet.policyValidUntil != null) {
115570
- credentials.policyValidUntil = BigInt(approvedWallet.policyValidUntil);
115571
- }
115572
- }
115573
- } catch (backendError) {
115875
+ let activeWallet;
115876
+ let grantChanged = false;
115877
+ try {
115878
+ activeWallet = selectNewestApprovedWalletAmong(wallets, [
115879
+ ctx.eoaAddress,
115880
+ ...ctx.candidateAddresses ?? []
115881
+ ]);
115882
+ if (activeWallet) {
115883
+ const newestId = String(activeWallet.id);
115884
+ const rowWalletAddress = activeWallet.walletAddress || activeWallet.kernelAccountAddress;
115885
+ const rowPaymentManagerAddress = activeWallet.paymentManagerAddress;
115886
+ const rowPolicyId = activeWallet.policyAssociated != null ? String(activeWallet.policyAssociated) : void 0;
115887
+ grantChanged = !!virtualWalletId && virtualWalletId !== newestId;
115888
+ if (grantChanged) {
115889
+ console.error(
115890
+ `[start_session] The active grant moved on \u2014 virtual wallet ${virtualWalletId} -> ${newestId}. Adopting the newest approved grant and dropping the session state derived from the old one.`
115891
+ );
115892
+ walletAddress = rowWalletAddress;
115893
+ paymentManagerAddress = rowPaymentManagerAddress;
115894
+ policyId = rowPolicyId;
115895
+ } else {
115896
+ walletAddress = rowWalletAddress || walletAddress;
115897
+ paymentManagerAddress = rowPaymentManagerAddress || paymentManagerAddress;
115898
+ policyId = rowPolicyId ?? policyId;
115899
+ }
115900
+ virtualWalletId = newestId;
115901
+ if (typeof activeWallet.address === "string" && activeWallet.address.trim() !== "") {
115902
+ credentials.signerAddress = activeWallet.address;
115903
+ }
115904
+ credentials.walletAddress = walletAddress;
115905
+ credentials.virtualWalletId = virtualWalletId;
115906
+ credentials.paymentManagerAddress = paymentManagerAddress;
115907
+ credentials.policyId = policyId;
115908
+ credentials.authorizationStatus = "approved";
115909
+ if (activeWallet.policyName != null) {
115910
+ credentials.policyName = activeWallet.policyName;
115911
+ }
115912
+ if (activeWallet.policyValidUntil != null) {
115913
+ credentials.policyValidUntil = BigInt(activeWallet.policyValidUntil);
115914
+ }
115915
+ } else if (walletsFetchFailed && virtualWalletId) {
115574
115916
  console.warn(
115575
- "[start_session] Could not check backend:",
115576
- backendError instanceof Error ? backendError.message : String(backendError)
115917
+ `[start_session] \u26A0\uFE0F could not verify the active grant \u2014 signing with the cached one (virtual wallet ${virtualWalletId}, policy ${policyId ?? "unknown"}). If a newer grant was approved since, spends are metered against the older one and may hit its spending limit.`
115577
115918
  );
115578
115919
  }
115920
+ } catch (backendError) {
115921
+ console.warn(
115922
+ "[start_session] Could not check backend:",
115923
+ backendError instanceof Error ? backendError.message : String(backendError)
115924
+ );
115579
115925
  }
115580
115926
  const mergedAuth = credentials.authorizationStatus !== void 0 ? credentials.authorizationStatus : ctx.authorizationStatus;
115581
115927
  console.error(
115582
115928
  `[start_session] State check \u2014 walletAddress: ${walletAddress}, virtualWalletId: ${virtualWalletId}, paymentManagerAddress: ${paymentManagerAddress}, policyId: ${policyId}`
115583
115929
  );
115584
115930
  if (mergedAuth === "expired") {
115585
- return { responseKey: "unlocked_expired", credentials, timingReport: timing.report() };
115931
+ return { responseKey: "unlocked_expired", credentials, timingReport: timing.report(), grantChanged };
115586
115932
  }
115587
115933
  if (walletAddress && virtualWalletId && paymentManagerAddress && policyId) {
115588
115934
  let privateKey = null;
115589
115935
  try {
115590
- privateKey = readVaultPrivateKey(ctx.walletKeystoreJson, ctx.passphrase);
115936
+ const signerAddress = credentials.signerAddress ?? ctx.eoaAddress;
115937
+ privateKey = ctx.resolveSigningKey && signerAddress ? ctx.resolveSigningKey(signerAddress) : readVaultPrivateKey(ctx.walletKeystoreJson, ctx.passphrase);
115591
115938
  const { virtualWalletsManagers: virtualWalletsManagers2, financialAccounts: financialAccounts2 } = ctx.sdk;
115592
115939
  const walletData = await virtualWalletsManagers2.getVirtualWalletDataByApiKey(
115593
115940
  ctx.apiKey,
@@ -115604,12 +115951,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115604
115951
  }
115605
115952
  credentials.walletAddress = walletAddress;
115606
115953
  credentials.paymentManagerAddress = paymentManagerAddress;
115607
- const approvedWallet = wallets.find(
115608
- (w) => w.address?.toLowerCase() === ctx.eoaAddress?.toLowerCase() && w.status === "approved"
115609
- );
115610
- const preloadedWalletData = approvedWallet != null && approvedWallet.policyValidAfter != null && approvedWallet.policyValidUntil != null ? {
115611
- policyValidAfter: Number(approvedWallet.policyValidAfter),
115612
- policyValidUntil: Number(approvedWallet.policyValidUntil)
115954
+ const preloadedWalletData = activeWallet != null && activeWallet.policyValidAfter != null && activeWallet.policyValidUntil != null ? {
115955
+ policyValidAfter: Number(activeWallet.policyValidAfter),
115956
+ policyValidUntil: Number(activeWallet.policyValidUntil)
115613
115957
  } : void 0;
115614
115958
  if (ctx.readPolicyActiveOnchain) {
115615
115959
  const nowSeconds = Math.floor(Date.now() / 1e3);
@@ -115631,7 +115975,7 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115631
115975
  maxAttempts
115632
115976
  });
115633
115977
  if (bootAction === "reauthorize") {
115634
- return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report() };
115978
+ return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report(), grantChanged };
115635
115979
  }
115636
115980
  }
115637
115981
  const { kernelClient: virtualWalletKernelAccountClient, permissionPlugin: virtualWalletPermissionPlugin, kernelDomain } = await virtualWalletsManagers2.configureVirtualWalletKernelAccount(
@@ -115661,13 +116005,14 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115661
116005
  console.warn("[start_session] \u26A0\uFE0F Could not fetch credit token address (Path B routing disabled):", e);
115662
116006
  }
115663
116007
  }
115664
- return { responseKey: "unlocked_and_authorized", credentials, timingReport: timing.report() };
116008
+ return { responseKey: "unlocked_and_authorized", credentials, timingReport: timing.report(), grantChanged };
115665
116009
  } catch (error) {
115666
116010
  console.error("\u274C [start_session] Kernel client creation failed:", error);
115667
116011
  return {
115668
116012
  responseKey: "unlocked_not_configured",
115669
116013
  credentials,
115670
116014
  timingReport: timing.report(),
116015
+ grantChanged,
115671
116016
  kernelErrorMessage: error instanceof Error ? error.message : String(error)
115672
116017
  };
115673
116018
  } finally {
@@ -115675,9 +116020,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115675
116020
  }
115676
116021
  }
115677
116022
  if (!walletsFetchFailed && wallets.length === 0) {
115678
- return { responseKey: "unlocked_provisioning_pending", credentials, timingReport: timing.report() };
116023
+ return { responseKey: "unlocked_provisioning_pending", credentials, timingReport: timing.report(), grantChanged };
115679
116024
  }
115680
- return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report() };
116025
+ return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report(), grantChanged };
115681
116026
  }
115682
116027
  var WALLET_SCOPED_CREDENTIAL_KEYS = [
115683
116028
  "virtualWalletId",
@@ -115707,9 +116052,9 @@ function clearWalletScopedCredentials(target) {
115707
116052
  function sameAddress(a, b) {
115708
116053
  return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
115709
116054
  }
115710
- function mergeStartSessionCredentials(target, patch, responseKey) {
116055
+ function mergeStartSessionCredentials(target, patch, responseKey, options = {}) {
115711
116056
  const walletChanged = patch.eoaAddress !== void 0 && !sameAddress(patch.eoaAddress, target.eoaAddress);
115712
- if (walletChanged) {
116057
+ if (walletChanged || options.grantChanged) {
115713
116058
  clearWalletScopedCredentials(target);
115714
116059
  }
115715
116060
  if (patch.walletKeystoreJson !== void 0) {
@@ -116889,13 +117234,13 @@ init_local_state();
116889
117234
  // src/mcp-server/materialize-task.ts
116890
117235
  init_esm_shims();
116891
117236
  init_paths();
116892
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
116893
- import { join as join8 } from "path";
117237
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
117238
+ import { join as join9 } from "path";
116894
117239
 
116895
117240
  // src/loops/state-docs.ts
116896
117241
  init_esm_shims();
116897
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
116898
- import { join as join7 } from "path";
117242
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
117243
+ import { join as join8 } from "path";
116899
117244
 
116900
117245
  // src/loops/shipback.ts
116901
117246
  init_esm_shims();
@@ -117328,12 +117673,12 @@ function docMergeRefusal(boot, fresh, next, cleanAppend) {
117328
117673
  async function shipBackStateDocs(sdk, apiKey, slug, dir, boot) {
117329
117674
  const outcomes = [];
117330
117675
  for (const doc of boot) {
117331
- const path2 = join7(dir, doc.filename);
117332
- if (!existsSync9(path2)) {
117676
+ const path2 = join8(dir, doc.filename);
117677
+ if (!existsSync10(path2)) {
117333
117678
  outcomes.push({ key: doc.key, outcome: "unchanged" });
117334
117679
  continue;
117335
117680
  }
117336
- const materialized = readFileSync8(path2, "utf-8");
117681
+ const materialized = readFileSync9(path2, "utf-8");
117337
117682
  const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117338
117683
  const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
117339
117684
  if (fresh === void 0) {
@@ -117403,20 +117748,20 @@ var TASK_DEFINITION_FILES = [
117403
117748
  ];
117404
117749
  function materializeTask(task, runId) {
117405
117750
  const dir = loopFireDir(task.slug, runId);
117406
- mkdirSync6(dir, { recursive: true, mode: 448 });
117751
+ mkdirSync7(dir, { recursive: true, mode: 448 });
117407
117752
  const files = {};
117408
117753
  const skipped = [];
117409
117754
  for (const [label2, filename, field] of TASK_DEFINITION_FILES) {
117410
117755
  const body = task[field];
117411
117756
  if (typeof body === "string" && body.length > 0) {
117412
- writeFileSync8(join8(dir, filename), body, { mode: 384 });
117757
+ writeFileSync8(join9(dir, filename), body, { mode: 384 });
117413
117758
  files[label2] = filename;
117414
117759
  } else {
117415
117760
  skipped.push(label2);
117416
117761
  }
117417
117762
  }
117418
117763
  writeFileSync8(
117419
- join8(dir, "STATUS.md"),
117764
+ join9(dir, "STATUS.md"),
117420
117765
  `# STATUS \u2014 ${task.slug}
117421
117766
 
117422
117767
  task_id: ${task.id}
@@ -117447,7 +117792,7 @@ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117447
117792
  notes.push(`memory doc '${f.key}' not materialized: ${f.reason}`);
117448
117793
  }
117449
117794
  for (const doc of fetched) {
117450
- writeFileSync8(join8(dir, doc.filename), doc.body, { mode: 384 });
117795
+ writeFileSync8(join9(dir, doc.filename), doc.body, { mode: 384 });
117451
117796
  files[`doc:${doc.key}`] = doc.filename;
117452
117797
  notes.push(describeSource(doc));
117453
117798
  }
@@ -117461,14 +117806,14 @@ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117461
117806
  }
117462
117807
 
117463
117808
  // src/mcp-server/index.ts
117464
- import { existsSync as existsSync12 } from "fs";
117809
+ import { existsSync as existsSync13 } from "fs";
117465
117810
 
117466
117811
  // src/loops/dashboard.ts
117467
117812
  init_esm_shims();
117468
117813
  import { createServer as createServer2 } from "http";
117469
117814
  import * as realFs from "fs";
117470
117815
  import { spawn as realSpawn } from "child_process";
117471
- import { join as join9 } from "path";
117816
+ import { join as join10 } from "path";
117472
117817
  var DEFAULT_PORT = 4477;
117473
117818
  var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
117474
117819
  var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
@@ -117529,14 +117874,14 @@ function startDashboardServer(args) {
117529
117874
  const data = {};
117530
117875
  for (const name of files) {
117531
117876
  try {
117532
- const p = join9(args.loopDir, name);
117877
+ const p = join10(args.loopDir, name);
117533
117878
  if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
117534
117879
  } catch {
117535
117880
  }
117536
117881
  }
117537
117882
  const state = {};
117538
117883
  try {
117539
- const p = join9(args.loopDir, ".state", "fires.jsonl");
117884
+ const p = join10(args.loopDir, ".state", "fires.jsonl");
117540
117885
  if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
117541
117886
  } catch {
117542
117887
  }
@@ -117935,7 +118280,7 @@ function injectDefaultDashboard(loop2) {
117935
118280
 
117936
118281
  // src/loops/memory-verbs.ts
117937
118282
  init_esm_shims();
117938
- import { readFileSync as readFileSync9 } from "fs";
118283
+ import { readFileSync as readFileSync10 } from "fs";
117939
118284
 
117940
118285
  // src/loops/sdk.ts
117941
118286
  init_esm_shims();
@@ -118195,7 +118540,7 @@ async function taskMemoryAppendVerb(rawSlug, opts) {
118195
118540
  const archived = opts.archived === true;
118196
118541
  const note = opts.note?.trim() || void 0;
118197
118542
  const scope = parseScopeFlag(opts.scope, false);
118198
- const content = opts.file !== void 0 ? readFileSync9(opts.file, "utf-8") : opts.content ?? "";
118543
+ const content = opts.file !== void 0 ? readFileSync10(opts.file, "utf-8") : opts.content ?? "";
118199
118544
  if (!content) {
118200
118545
  throw new Error("nothing to store: pass --content <text> or --file <path>");
118201
118546
  }
@@ -118332,14 +118677,14 @@ ${full.markdownBody ?? ""}`;
118332
118677
 
118333
118678
  // src/mcp-server/read-file-body.ts
118334
118679
  init_esm_shims();
118335
- import { readFileSync as readFileSync10 } from "fs";
118680
+ import { readFileSync as readFileSync11 } from "fs";
118336
118681
  import { resolve as resolvePath } from "path";
118337
118682
  function readMarkdownFile(filePath) {
118338
118683
  const raw = typeof filePath === "string" ? filePath.trim() : "";
118339
118684
  if (!raw) throw new Error("filePath is empty.");
118340
118685
  const abs = resolvePath(raw);
118341
118686
  try {
118342
- return readFileSync10(abs, "utf8");
118687
+ return readFileSync11(abs, "utf8");
118343
118688
  } catch (err) {
118344
118689
  const reason = err instanceof Error ? err.message : String(err);
118345
118690
  throw new Error(`Could not read file at "${abs}": ${reason}`);
@@ -118668,7 +119013,7 @@ function enforceResponseCap(payload) {
118668
119013
 
118669
119014
  // src/mcp-server/delegate-tools.ts
118670
119015
  init_esm_shims();
118671
- import { existsSync as existsSync10, statSync as statSync2 } from "fs";
119016
+ import { existsSync as existsSync11, statSync as statSync2 } from "fs";
118672
119017
  import { z as z3 } from "zod";
118673
119018
 
118674
119019
  // src/delegate/jobs.ts
@@ -118770,7 +119115,7 @@ import * as nodeFs from "fs";
118770
119115
  import { execFileSync } from "child_process";
118771
119116
  import { createHash } from "crypto";
118772
119117
  import { homedir as homedir5 } from "os";
118773
- import { join as join10 } from "path";
119118
+ import { join as join11 } from "path";
118774
119119
  var OPENCODE_VERSION = "1.18.13";
118775
119120
  var OPENCODE_CHECKSUMS = {
118776
119121
  "darwin-arm64": "6a85ae6de1aeb8e39ae4d977337b03f49168c2a827ee37b6f82c39471d711c63",
@@ -118792,10 +119137,10 @@ function resolveOpencodePlatform(platform, arch) {
118792
119137
  return void 0;
118793
119138
  }
118794
119139
  function opencodeBinDir(home = homedir5()) {
118795
- return join10(home, `.ametyst${ENV_SUFFIX}`, "bin");
119140
+ return join11(home, `.ametyst${ENV_SUFFIX}`, "bin");
118796
119141
  }
118797
119142
  function opencodeBinaryPath(home = homedir5()) {
118798
- return join10(opencodeBinDir(home), `opencode-${OPENCODE_VERSION}`);
119143
+ return join11(opencodeBinDir(home), `opencode-${OPENCODE_VERSION}`);
118799
119144
  }
118800
119145
  function opencodeSidecarPath(home = homedir5()) {
118801
119146
  return `${opencodeBinaryPath(home)}.sha256`;
@@ -118850,7 +119195,7 @@ async function ensureOpencodeBinary(deps = {}) {
118850
119195
  const bytes = new Uint8Array(await res.arrayBuffer());
118851
119196
  const expected = (deps.checksums ?? OPENCODE_CHECKSUMS)[key];
118852
119197
  const actual = sha256Hex(bytes);
118853
- const tmpArchive = join10(binDir, `.${artifact}.download-${process.pid}`);
119198
+ const tmpArchive = join11(binDir, `.${artifact}.download-${process.pid}`);
118854
119199
  fs.writeFileSync(tmpArchive, bytes);
118855
119200
  if (actual !== expected) {
118856
119201
  fs.rmSync(tmpArchive, { force: true });
@@ -118858,7 +119203,7 @@ async function ensureOpencodeBinary(deps = {}) {
118858
119203
  `opencode bootstrap: checksum mismatch for ${artifact} (expected ${expected}, got ${actual}). The download was deleted; refusing to install an unverified binary.`
118859
119204
  );
118860
119205
  }
118861
- const tmpExtractDir = join10(binDir, `.extract-${OPENCODE_VERSION}-${process.pid}`);
119206
+ const tmpExtractDir = join11(binDir, `.extract-${OPENCODE_VERSION}-${process.pid}`);
118862
119207
  fs.rmSync(tmpExtractDir, { recursive: true, force: true });
118863
119208
  fs.mkdirSync(tmpExtractDir, { recursive: true, mode: 448 });
118864
119209
  try {
@@ -118867,7 +119212,7 @@ async function ensureOpencodeBinary(deps = {}) {
118867
119212
  } else {
118868
119213
  exec("tar", ["-xzf", tmpArchive, "-C", tmpExtractDir]);
118869
119214
  }
118870
- const extracted = join10(tmpExtractDir, "opencode");
119215
+ const extracted = join11(tmpExtractDir, "opencode");
118871
119216
  if (!fs.existsSync(extracted)) {
118872
119217
  const entries = fs.readdirSync(tmpExtractDir).join(", ") || "<empty>";
118873
119218
  throw new Error(
@@ -118938,15 +119283,15 @@ import { randomUUID } from "crypto";
118938
119283
  init_esm_shims();
118939
119284
  init_paths();
118940
119285
  import { homedir as homedir6 } from "os";
118941
- import { join as join11 } from "path";
119286
+ import { join as join12 } from "path";
118942
119287
  function bridgeRunDir(home = homedir6()) {
118943
- return join11(home, `.ametyst${ENV_SUFFIX}`, "run");
119288
+ return join12(home, `.ametyst${ENV_SUFFIX}`, "run");
118944
119289
  }
118945
119290
  function bridgeSocketPath(pid, home = homedir6()) {
118946
- return join11(bridgeRunDir(home), `delegate-${pid}.sock`);
119291
+ return join12(bridgeRunDir(home), `delegate-${pid}.sock`);
118947
119292
  }
118948
119293
  function bridgeNoncePath(pid, home = homedir6()) {
118949
- return join11(bridgeRunDir(home), `delegate-${pid}.nonce`);
119294
+ return join12(bridgeRunDir(home), `delegate-${pid}.nonce`);
118950
119295
  }
118951
119296
  function bridgeNoncePathForSocket(socketPath) {
118952
119297
  return socketPath.replace(/\.sock$/, ".nonce");
@@ -118968,7 +119313,7 @@ function listBridgeSockets(fs, home = homedir6()) {
118968
119313
  const entries = [];
118969
119314
  for (const name of names) {
118970
119315
  if (pidFromSocketName(name) === void 0) continue;
118971
- const path2 = join11(dir, name);
119316
+ const path2 = join12(dir, name);
118972
119317
  try {
118973
119318
  entries.push({ path: path2, mtimeMs: fs.statSync(path2).mtimeMs });
118974
119319
  } catch {
@@ -119317,7 +119662,7 @@ async function startShim(deps) {
119317
119662
  init_esm_shims();
119318
119663
  import * as nodeFs3 from "fs";
119319
119664
  import { tmpdir } from "os";
119320
- import { join as join12 } from "path";
119665
+ import { join as join13 } from "path";
119321
119666
  var DELEGATE_PROVIDER_ID = "ametyst";
119322
119667
  var DELEGATE_SHIM_API_KEY = "ametyst-local-shim";
119323
119668
  function buildDelegateConfig(opts) {
@@ -119341,8 +119686,8 @@ function delegateModelRef(model) {
119341
119686
  }
119342
119687
  function writeTempDelegateConfig(config, deps = {}) {
119343
119688
  const fs = deps.fs ?? nodeFs3;
119344
- const dir = fs.mkdtempSync(join12(deps.tmp ?? tmpdir(), "ametyst-delegate-"));
119345
- const path2 = join12(dir, "opencode.json");
119689
+ const dir = fs.mkdtempSync(join13(deps.tmp ?? tmpdir(), "ametyst-delegate-"));
119690
+ const path2 = join13(dir, "opencode.json");
119346
119691
  fs.writeFileSync(path2, `${JSON.stringify(config, null, 2)}
119347
119692
  `, { mode: 384 });
119348
119693
  let done = false;
@@ -119411,7 +119756,7 @@ init_paths();
119411
119756
  import * as nodeFs4 from "fs";
119412
119757
  import { execFileSync as execFileSync2 } from "child_process";
119413
119758
  import { homedir as homedir8 } from "os";
119414
- import { basename as basename4, join as join13 } from "path";
119759
+ import { basename as basename4, join as join14 } from "path";
119415
119760
  var DELEGATED_CHILD_ENV_VAR = "AMETYST_DELEGATED";
119416
119761
  var SPEND_GRANT_TOKEN_ENV_VAR = "AMETYST_DELEGATE_SPEND_TOKEN";
119417
119762
  var SPEND_KILL_SWITCH_ENV_VAR = "AMETYST_DELEGATE_NO_SPEND";
@@ -119493,11 +119838,11 @@ function isDelegatedByAncestry(deps = {}) {
119493
119838
  return classifyAncestry(deps) === "delegated";
119494
119839
  }
119495
119840
  function delegateChildConfigHome(home = homedir8()) {
119496
- return join13(home, `.ametyst${ENV_SUFFIX}`, "delegate-xdg");
119841
+ return join14(home, `.ametyst${ENV_SUFFIX}`, "delegate-xdg");
119497
119842
  }
119498
119843
  function ensureChildConfigHome(fs = nodeFs4, home = homedir8()) {
119499
119844
  const root2 = delegateChildConfigHome(home);
119500
- const inner = join13(root2, "opencode");
119845
+ const inner = join14(root2, "opencode");
119501
119846
  if (!fs.existsSync(inner)) fs.mkdirSync(inner, { recursive: true, mode: 448 });
119502
119847
  return root2;
119503
119848
  }
@@ -119507,7 +119852,7 @@ function userOpencodeConfigDir(deps = {}) {
119507
119852
  const env = deps.env ?? process.env;
119508
119853
  const home = deps.home ?? homedir8();
119509
119854
  const xdg = env.XDG_CONFIG_HOME?.trim();
119510
- return join13(xdg ? xdg : join13(home, ".config"), "opencode");
119855
+ return join14(xdg ? xdg : join14(home, ".config"), "opencode");
119511
119856
  }
119512
119857
  function stripJsonComments(text) {
119513
119858
  let out = "";
@@ -119551,9 +119896,9 @@ function readUserMcpServers(deps = {}) {
119551
119896
  const warn = deps.warn ?? ((line) => console.error(line));
119552
119897
  const home = deps.home ?? homedir8();
119553
119898
  const dir = userOpencodeConfigDir(deps);
119554
- if (dir === join13(delegateChildConfigHome(home), "opencode")) return {};
119899
+ if (dir === join14(delegateChildConfigHome(home), "opencode")) return {};
119555
119900
  for (const filename of USER_OPENCODE_CONFIG_FILENAMES) {
119556
- const path2 = join13(dir, filename);
119901
+ const path2 = join14(dir, filename);
119557
119902
  let raw;
119558
119903
  try {
119559
119904
  if (!fs.existsSync(path2)) continue;
@@ -120651,7 +120996,7 @@ init_esm_shims();
120651
120996
  import * as nodeFs6 from "fs";
120652
120997
  import { createHash as createHash2, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "crypto";
120653
120998
  import { homedir as homedir10 } from "os";
120654
- import { join as join14 } from "path";
120999
+ import { join as join15 } from "path";
120655
121000
  var SPEND_GRANT_RECORD_VERSION = 1;
120656
121001
  var TOKEN_BYTES = 32;
120657
121002
  var SPEND_GRANT_GRACE_MS = 5 * 6e4;
@@ -120659,7 +121004,7 @@ function spendGrantPath(jobId, home = homedir10()) {
120659
121004
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(jobId)) {
120660
121005
  throw new Error(`refusing to build a grant path for job id ${JSON.stringify(jobId)}`);
120661
121006
  }
120662
- return join14(bridgeRunDir(home), `spend-grant-${jobId}.json`);
121007
+ return join15(bridgeRunDir(home), `spend-grant-${jobId}.json`);
120663
121008
  }
120664
121009
  function hashSpendGrantToken(token) {
120665
121010
  return createHash2("sha256").update(token, "utf-8").digest("hex");
@@ -120765,13 +121110,13 @@ init_esm_shims();
120765
121110
  init_paths();
120766
121111
  import * as nodeFs7 from "fs";
120767
121112
  import { homedir as homedir11 } from "os";
120768
- import { dirname as dirname6, join as join15 } from "path";
121113
+ import { dirname as dirname6, join as join16 } from "path";
120769
121114
  var DELEGATE_CREDIT_CAPABILITY = "credit";
120770
121115
  var DELEGATE_CREDIT_PRICE_USD = 1;
120771
121116
  var DELEGATE_CREDIT_REQUEST_BODY = "{}";
120772
121117
  var DELEGATE_CREDIT_SPEND_SNIPPET = `spend({ merchant_slug: "${DELEGATE_MERCHANT_SLUG}", capability: "${DELEGATE_CREDIT_CAPABILITY}", inputs: "${DELEGATE_CREDIT_REQUEST_BODY}" })`;
120773
121118
  function creditLedgerPath(home = homedir11()) {
120774
- return join15(home, `.ametyst${ENV_SUFFIX}`, "run", "delegate-credit.json");
121119
+ return join16(home, `.ametyst${ENV_SUFFIX}`, "run", "delegate-credit.json");
120775
121120
  }
120776
121121
  function readCreditLedger(fs = nodeFs7, path2 = creditLedgerPath()) {
120777
121122
  try {
@@ -121162,7 +121507,7 @@ function registerDelegateTools(deps) {
121162
121507
  const validated = validateStartInput(params, {
121163
121508
  isDirectory: (path2) => {
121164
121509
  try {
121165
- return existsSync10(path2) && statSync2(path2).isDirectory();
121510
+ return existsSync11(path2) && statSync2(path2).isDirectory();
121166
121511
  } catch {
121167
121512
  return false;
121168
121513
  }
@@ -121417,7 +121762,7 @@ init_resolve();
121417
121762
 
121418
121763
  // src/mcp-server/identity-reload.ts
121419
121764
  init_esm_shims();
121420
- import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
121765
+ import { readFileSync as readFileSync13, statSync as statSync3 } from "fs";
121421
121766
  var NO_VAULT = "absent";
121422
121767
  var IDENTITY_CHECK_THROTTLE_MS = 1e3;
121423
121768
  function fingerprintVault(stat) {
@@ -121438,7 +121783,11 @@ var IdentityWatch = class {
121438
121783
  stat;
121439
121784
  /** False for an env-supplied key: `poll` is then a guaranteed no-op. */
121440
121785
  armed;
121786
+ configPath;
121787
+ readWorkspaceKey;
121441
121788
  fingerprint;
121789
+ configStamp;
121790
+ workspaceKey;
121442
121791
  lastCheckedAt = null;
121443
121792
  constructor(options) {
121444
121793
  this.vaultPath = options.vaultPath;
@@ -121446,12 +121795,43 @@ var IdentityWatch = class {
121446
121795
  this.throttleMs = options.throttleMs ?? IDENTITY_CHECK_THROTTLE_MS;
121447
121796
  this.stat = options.stat ?? statVaultFile;
121448
121797
  this.armed = options.source !== "env";
121449
- this.fingerprint = fingerprintVault(this.stat(this.vaultPath));
121798
+ this.configPath = options.configPath ?? null;
121799
+ this.readWorkspaceKey = options.readWorkspaceKey ?? null;
121800
+ this.configStamp = this.configPath ? fingerprintVault(this.stat(this.configPath)) : NO_VAULT;
121801
+ this.workspaceKey = this.readWorkspaceKeySafely();
121802
+ this.fingerprint = this.composeFingerprint();
121803
+ }
121804
+ /** Never let a config read take the watch — or the tool call behind it — down. */
121805
+ readWorkspaceKeySafely() {
121806
+ if (!this.readWorkspaceKey) return null;
121807
+ try {
121808
+ return this.readWorkspaceKey();
121809
+ } catch {
121810
+ return null;
121811
+ }
121812
+ }
121813
+ /**
121814
+ * Vault fingerprint AND workspace key in one string, so `poll` stays a single
121815
+ * comparison and neither half can change without being noticed.
121816
+ *
121817
+ * A watch with NO workspace reader appends nothing, so its fingerprint stays
121818
+ * exactly the string it was before 0.3.6 (`"<mtime>:<size>"` / `"absent"`).
121819
+ * That is not cosmetic: the fingerprint is an observable of this class, and a
121820
+ * vault-only watch must be indistinguishable from the one that shipped.
121821
+ */
121822
+ composeFingerprint() {
121823
+ const vault = fingerprintVault(this.stat(this.vaultPath));
121824
+ if (!this.readWorkspaceKey) return vault;
121825
+ return `${vault}|ws:${this.workspaceKey ?? "unbound"}`;
121450
121826
  }
121451
121827
  /** The fingerprint this watch currently considers current. */
121452
121828
  currentFingerprint() {
121453
121829
  return this.fingerprint;
121454
121830
  }
121831
+ /** The workspace key this watch last read from the config. */
121832
+ currentWorkspaceKey() {
121833
+ return this.workspaceKey;
121834
+ }
121455
121835
  /**
121456
121836
  * One throttled `stat`. Returns the change when the vault file differs from
121457
121837
  * the fingerprint on record — and ADOPTS it in the same step, so a reload the
@@ -121464,7 +121844,14 @@ var IdentityWatch = class {
121464
121844
  const at = this.now();
121465
121845
  if (this.lastCheckedAt !== null && at - this.lastCheckedAt < this.throttleMs) return null;
121466
121846
  this.lastCheckedAt = at;
121467
- const next = fingerprintVault(this.stat(this.vaultPath));
121847
+ if (this.configPath) {
121848
+ const stamp = fingerprintVault(this.stat(this.configPath));
121849
+ if (stamp !== this.configStamp) {
121850
+ this.configStamp = stamp;
121851
+ this.workspaceKey = this.readWorkspaceKeySafely();
121852
+ }
121853
+ }
121854
+ const next = this.composeFingerprint();
121468
121855
  if (next === this.fingerprint) return null;
121469
121856
  const from14 = this.fingerprint;
121470
121857
  this.fingerprint = next;
@@ -121480,7 +121867,7 @@ var IdentityWatch = class {
121480
121867
  */
121481
121868
  readVault() {
121482
121869
  try {
121483
- return readFileSync12(this.vaultPath, "utf-8");
121870
+ return readFileSync13(this.vaultPath, "utf-8");
121484
121871
  } catch {
121485
121872
  return null;
121486
121873
  }
@@ -121488,6 +121875,7 @@ var IdentityWatch = class {
121488
121875
  };
121489
121876
 
121490
121877
  // src/mcp-server/index.ts
121878
+ init_grant_keystore();
121491
121879
  function buildDeclaredConnectorsSentence(catalog, unreadable) {
121492
121880
  const read = catalog === void 0 ? { connectors: cachedCatalogOrEmpty(), unreadable: mirrorGap() } : { connectors: catalog, unreadable: unreadable ?? [] };
121493
121881
  const gap = describeCatalogGap(read.unreadable);
@@ -121498,6 +121886,39 @@ function buildDeclaredConnectorsSentence(catalog, unreadable) {
121498
121886
  return `Currently declared: ${declared}. Those are the operations picked out as most useful, NOT the limit: each provider's own surface is larger, and any operation it advertises is callable by its exact name. If the one you want is not listed, try it anyway \u2014 an operation that really does not exist comes back as a typed \`unknown_operation\` with the alternatives.` + (gap === "" ? "" : ` ${gap}`);
121499
121887
  }
121500
121888
  var currentCredentials = {};
121889
+ function vaultIsEncrypted() {
121890
+ return !plaintextVaultMode;
121891
+ }
121892
+ function ownedSigningAddresses() {
121893
+ const addresses = [];
121894
+ if (currentCredentials.eoaAddress) addresses.push(currentCredentials.eoaAddress);
121895
+ try {
121896
+ addresses.push(...listGrantAddresses({ workspaceId: readBoundWorkspace() }));
121897
+ } catch (err) {
121898
+ console.error(
121899
+ `\u26A0\uFE0F could not read the per-grant keystore \u2014 only the install EOA is selectable: ${err instanceof Error ? err.message : String(err)}`
121900
+ );
121901
+ }
121902
+ return addresses;
121903
+ }
121904
+ function isOwnedSigningAddress(address) {
121905
+ if (typeof address !== "string" || address.trim() === "") return false;
121906
+ const needle = address.toLowerCase();
121907
+ return ownedSigningAddresses().some((a) => a.toLowerCase() === needle);
121908
+ }
121909
+ function signingKeyForAddress(address, passphrase) {
121910
+ return resolveGrantSigningKey({
121911
+ address,
121912
+ vaultJson: currentCredentials.walletKeystoreJson,
121913
+ vaultAddress: currentCredentials.eoaAddress,
121914
+ // ⛔ THE CALLER'S PASSPHRASE WINS. `start_session` resolves the key DURING
121915
+ // `runStartSession`, before `mergeStartSessionCredentials` has put the
121916
+ // passphrase into `currentCredentials` — so reading only the field would
121917
+ // make every encrypted-vault unlock fail with "a passphrase is required"
121918
+ // on the very call that was handed one.
121919
+ passphrase: passphrase ?? currentCredentials.passphrase
121920
+ });
121921
+ }
121501
121922
  var plaintextVaultMode = false;
121502
121923
  function refreshPlaintextVaultMode() {
121503
121924
  const vault = currentCredentials.walletKeystoreJson;
@@ -122519,7 +122940,16 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
122519
122940
  currentCredentials.signerAddress = eoaAddress;
122520
122941
  currentCredentials.eoaAddress = eoaAddress;
122521
122942
  currentCredentials.apiKey = config.apiKey;
122522
- identityWatch = new IdentityWatch({ vaultPath: VAULT_PATH, source: config.apiKeySource ?? null });
122943
+ identityWatch = new IdentityWatch({
122944
+ vaultPath: VAULT_PATH,
122945
+ source: config.apiKeySource ?? null,
122946
+ // The workspace half (0.3.6): a `login`/`init --claim` that re-binds this
122947
+ // install to a DIFFERENT workspace changes the recorded key, and the same
122948
+ // reload that follows a swapped `wallet.json` drops the state cached for
122949
+ // the old one. See ./identity-reload.ts.
122950
+ configPath: CONFIG_PATH,
122951
+ readWorkspaceKey: readBoundWorkspace
122952
+ });
122523
122953
  mcpEventLogger = new McpEventLogger(
122524
122954
  () => getSDK(),
122525
122955
  () => currentCredentials.apiKey,
@@ -122605,10 +123035,8 @@ async function createKernelClientFromVault() {
122605
123035
  throw new Error("No passphrase or keystore available");
122606
123036
  }
122607
123037
  if (!cliConfig) throw new Error("CLI config not initialized");
122608
- const { readVaultPrivateKey: readVaultPrivateKey2 } = await Promise.resolve().then(() => (init_core(), core_exports));
122609
- let privateKey = readVaultPrivateKey2(
122610
- currentCredentials.walletKeystoreJson,
122611
- currentCredentials.passphrase
123038
+ let privateKey = signingKeyForAddress(
123039
+ currentCredentials.signerAddress || currentCredentials.eoaAddress || ""
122612
123040
  );
122613
123041
  console.error(`${ts()} Wallet key loaded`);
122614
123042
  try {
@@ -122739,17 +123167,6 @@ var approvalWaitConfig = (() => {
122739
123167
  delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
122740
123168
  };
122741
123169
  })();
122742
- function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
122743
- if (!Array.isArray(wallets)) return void 0;
122744
- if (pendingWalletId) {
122745
- return wallets.find(
122746
- (w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved"
122747
- );
122748
- }
122749
- return wallets.find(
122750
- (w) => w?.address?.toLowerCase() === eoaAddress?.toLowerCase() && w?.status === "approved"
122751
- );
122752
- }
122753
123170
  async function tryResolvePendingApproval(probe) {
122754
123171
  if (currentCredentials.authorizationStatus === "approved") return true;
122755
123172
  if (currentCredentials.authorizationStatus !== "pending" || !currentCredentials.apiKey || !currentCredentials.eoaAddress) {
@@ -122760,12 +123177,17 @@ async function tryResolvePendingApproval(probe) {
122760
123177
  if (probe) probe.fetched = true;
122761
123178
  const approvedWallet = findCurrentApprovedWallet(
122762
123179
  freshWallets,
122763
- currentCredentials.eoaAddress,
123180
+ // Post-rotation the row belongs to the ephemeral key this request minted,
123181
+ // not to the install EOA — match on every address we hold a key for.
123182
+ ownedSigningAddresses(),
122764
123183
  currentCredentials.pendingWalletId
122765
123184
  );
122766
123185
  if (!approvedWallet) return false;
122767
123186
  console.error("\u2705 [approval-wait] Backend says approved \u2014 syncing");
122768
123187
  currentCredentials.authorizationStatus = "approved";
123188
+ if (typeof approvedWallet.address === "string" && approvedWallet.address.trim() !== "") {
123189
+ currentCredentials.signerAddress = approvedWallet.address;
123190
+ }
122769
123191
  currentCredentials.walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
122770
123192
  currentCredentials.virtualWalletId = String(approvedWallet.id);
122771
123193
  currentCredentials.paymentManagerAddress = approvedWallet.paymentManagerAddress;
@@ -122887,11 +123309,26 @@ async function applyStartSessionUnlock(passphrase, resetRequested) {
122887
123309
  persistVault: persistVault2,
122888
123310
  wipeVault: wipeVault2,
122889
123311
  sdk,
122890
- fetchWallets: () => fetchVirtualWalletsFromBackend(currentCredentials.apiKey),
123312
+ // FORCED REFRESH. `start_session` verifies the session's grant against the
123313
+ // backend (see `runStartSession`), and a verification run against the cache
123314
+ // this process loaded at boot verifies nothing: a grant approved since boot
123315
+ // would be invisible to exactly the check that exists to find it.
123316
+ fetchWallets: () => fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true),
123317
+ // ROTATION (0.3.6). Grant selection must span every address this install
123318
+ // holds a key for, and the key that signs must be the SELECTED row's —
123319
+ // not whatever `wallet.json` happens to contain. Both are injected so
123320
+ // `start-session.ts` keeps no dependency on the keystore's fs layer.
123321
+ candidateAddresses: ownedSigningAddresses(),
123322
+ resolveSigningKey: (address) => signingKeyForAddress(address, passphrase),
122891
123323
  readPolicyActiveOnchain: buildPolicyActiveOnchainReader(sdk)
122892
123324
  }
122893
123325
  );
122894
- const { walletChanged } = mergeStartSessionCredentials(currentCredentials, result.credentials, result.responseKey);
123326
+ const { walletChanged } = mergeStartSessionCredentials(
123327
+ currentCredentials,
123328
+ result.credentials,
123329
+ result.responseKey,
123330
+ { grantChanged: result.grantChanged }
123331
+ );
122895
123332
  refreshPlaintextVaultMode();
122896
123333
  if (walletChanged) {
122897
123334
  walletsCache = null;
@@ -123296,11 +123733,15 @@ server.tool(
123296
123733
  };
123297
123734
  }
123298
123735
  const { virtualWalletsManagers: virtualWalletsManagers2 } = await getSDK();
123299
- const result = await submitAccessRequest(
123736
+ const result = await submitRotatedAccessRequest(
123300
123737
  virtualWalletsManagers2,
123301
123738
  currentCredentials.apiKey,
123302
- currentCredentials.eoaAddress,
123303
- policyId
123739
+ policyId,
123740
+ createGrantKeyIssuer({
123741
+ encrypted: vaultIsEncrypted(),
123742
+ passphrase: currentCredentials.passphrase,
123743
+ workspaceId: readBoundWorkspace()
123744
+ })
123304
123745
  );
123305
123746
  currentCredentials.authorizationStatus = "pending";
123306
123747
  currentCredentials.pendingWalletId = result.id ? String(result.id) : void 0;
@@ -123358,7 +123799,7 @@ server.tool(
123358
123799
  server.tool(
123359
123800
  {
123360
123801
  name: "getWalletStatus",
123361
- description: "Get wallet status: auth, balance, policy, allowlist, WHICH WORKSPACE this server is acting in, and the last 10 transactions (amount, merchant, timestamp, status). `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.",
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.",
123362
123803
  inputs: []
123363
123804
  },
123364
123805
  async () => {
@@ -123390,10 +123831,9 @@ server.tool(
123390
123831
  fetchVirtualWalletsFromBackend(currentCredentials.apiKey, !refreshedFromBackend),
123391
123832
  fetchPoliciesFromBackend(currentCredentials.apiKey, true)
123392
123833
  ]);
123393
- const freshWallet = freshWallets.find(
123394
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
123395
- );
123396
- if (freshWallet && freshWallet.status === "approved") {
123834
+ const freshWallet = selectNewestApprovedWalletAmong(freshWallets, ownedSigningAddresses());
123835
+ const freshWalletIsActiveGrant = String(freshWallet?.id) === String(currentCredentials.virtualWalletId);
123836
+ if (freshWallet && freshWalletIsActiveGrant) {
123397
123837
  if (freshWallet.policyAssociated != null) {
123398
123838
  currentCredentials.policyId = String(freshWallet.policyAssociated);
123399
123839
  }
@@ -123412,8 +123852,24 @@ server.tool(
123412
123852
  authorized: currentCredentials.authorizationStatus === "approved",
123413
123853
  status: currentCredentials.authorizationStatus || "none",
123414
123854
  eoaAddress: currentCredentials.eoaAddress || null,
123855
+ // WHICH KEY IS SIGNING. Since 0.3.6 every grant gets its OWN ephemeral
123856
+ // session key, so `signerAddress` is no longer a synonym for
123857
+ // `eoaAddress`: it is the address of the approved row this session
123858
+ // adopted, and `eoaAddress` is the install identity in `wallet.json`.
123859
+ // They agree only for a pre-rotation grant. Reporting both is what makes
123860
+ // "which key signed this" readable from a payload instead of from a
123861
+ // nonce-key autopsy — the same lesson as `activeVirtualWalletId` below.
123862
+ signerAddress: currentCredentials.signerAddress || currentCredentials.eoaAddress || null,
123415
123863
  walletAddress: currentCredentials.walletAddress || null,
123416
123864
  kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient,
123865
+ // WHICH GRANT THIS SESSION SIGNS WITH. The 2026-09-01 incident — three
123866
+ // approved rows for one EOA, every session silently signing with the oldest
123867
+ // — was invisible in every payload: diagnosing it took a nonce-key autopsy.
123868
+ // These two ids are the row `start_session` actually selected, so the next
123869
+ // stale grant is a field to read rather than a forensic exercise. Null until
123870
+ // a session has adopted one.
123871
+ activeVirtualWalletId: currentCredentials.virtualWalletId != null ? String(currentCredentials.virtualWalletId) : null,
123872
+ activePolicyId: currentCredentials.policyId != null ? String(currentCredentials.policyId) : null,
123417
123873
  // WHICH WORKSPACE THIS IS. The 2026-08-31 incident produced writes into
123418
123874
  // the wrong workspace with nothing in any payload naming one — an agent
123419
123875
  // reading wallet status could not have told, and neither could a human
@@ -123427,9 +123883,7 @@ server.tool(
123427
123883
  }
123428
123884
  }
123429
123885
  if (walletsCache && currentCredentials.eoaAddress) {
123430
- const wallet = walletsCache.find(
123431
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
123432
- );
123886
+ const wallet = selectNewestApprovedWalletAmong(walletsCache, ownedSigningAddresses()) ?? walletsCache.find((w) => isOwnedSigningAddress(w.address));
123433
123887
  if (wallet) {
123434
123888
  const now = Math.floor(Date.now() / 1e3);
123435
123889
  const validUntil = wallet.policyValidUntil;
@@ -123953,7 +124407,7 @@ async function startLiveDashboard(entity, dir) {
123953
124407
  clearInterval(watcher);
123954
124408
  return;
123955
124409
  }
123956
- if (!existsSync12(dir)) {
124410
+ if (!existsSync13(dir)) {
123957
124411
  clearInterval(watcher);
123958
124412
  liveDashboards.delete(slug);
123959
124413
  try {
@@ -124900,10 +125354,13 @@ server.tool(
124900
125354
  }
124901
125355
  const approvedWallet = findCurrentApprovedWallet(
124902
125356
  wallets,
124903
- currentCredentials.eoaAddress,
125357
+ ownedSigningAddresses(),
124904
125358
  currentCredentials.pendingWalletId
124905
125359
  );
124906
125360
  if (approvedWallet && currentCredentials.authorizationStatus !== "approved") {
125361
+ if (typeof approvedWallet.address === "string" && approvedWallet.address.trim() !== "") {
125362
+ currentCredentials.signerAddress = approvedWallet.address;
125363
+ }
124907
125364
  currentCredentials.walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
124908
125365
  currentCredentials.virtualWalletId = String(approvedWallet.id);
124909
125366
  currentCredentials.paymentManagerAddress = approvedWallet.paymentManagerAddress;
@@ -125792,9 +126249,7 @@ async function reconcileApprovalStateOnConnect() {
125792
126249
  }
125793
126250
  try {
125794
126251
  const wallets = await fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true);
125795
- const approved = wallets.find(
125796
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase() && w.status === "approved"
125797
- );
126252
+ const approved = selectNewestApprovedWalletAmong(wallets, ownedSigningAddresses());
125798
126253
  if (!approved) return;
125799
126254
  if (typeof approved.id === "number" && hasProcessedApprovalEventId(approved.id)) return;
125800
126255
  console.error(
@@ -125809,9 +126264,7 @@ async function reconcileApprovalStateOnConnect() {
125809
126264
  policyOnchainPermissions: void 0
125810
126265
  });
125811
126266
  if (walletsCache) {
125812
- const idx = walletsCache.findIndex(
125813
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
125814
- );
126267
+ const idx = walletsCache.findIndex((w) => isOwnedSigningAddress(w.address));
125815
126268
  if (idx >= 0) walletsCache[idx] = { ...walletsCache[idx], status: "approved" };
125816
126269
  }
125817
126270
  if (typeof approved.id === "number") rememberProcessedApprovalEventId(approved.id);
@@ -125844,8 +126297,8 @@ async function setupWebSocketListener() {
125844
126297
  return;
125845
126298
  }
125846
126299
  const statusMatch = event.status === "approved" || event.status === "ok";
125847
- const addressMatch = event.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase();
125848
- console.error(`\u{1F50D} [WebSocket] statusMatch=${statusMatch} addressMatch=${addressMatch} eoaAddress=${currentCredentials.eoaAddress}`);
126300
+ const addressMatch = isOwnedSigningAddress(event.address);
126301
+ console.error(`\u{1F50D} [WebSocket] statusMatch=${statusMatch} addressMatch=${addressMatch} eoaAddress=${currentCredentials.eoaAddress} signerAddress=${currentCredentials.signerAddress}`);
125849
126302
  if (statusMatch && addressMatch) {
125850
126303
  try {
125851
126304
  await onAuthorizationApproved({
@@ -125857,9 +126310,7 @@ async function setupWebSocketListener() {
125857
126310
  policyOnchainPermissions: void 0
125858
126311
  });
125859
126312
  if (walletsCache) {
125860
- const idx = walletsCache.findIndex(
125861
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
125862
- );
126313
+ const idx = walletsCache.findIndex((w) => isOwnedSigningAddress(w.address));
125863
126314
  if (idx >= 0) {
125864
126315
  walletsCache[idx] = { ...walletsCache[idx], status: "approved" };
125865
126316
  }
@@ -126132,15 +126583,15 @@ function killPreviousServeInstances(deps) {
126132
126583
 
126133
126584
  // src/commands/autosync-skills.ts
126134
126585
  init_esm_shims();
126135
- import { existsSync as existsSync14 } from "fs";
126586
+ import { existsSync as existsSync15 } from "fs";
126136
126587
  import { homedir as homedir12 } from "os";
126137
- import { join as join18 } from "path";
126588
+ import { join as join19 } from "path";
126138
126589
 
126139
126590
  // src/compounds/sync-skills.ts
126140
126591
  init_esm_shims();
126141
126592
  init_paths();
126142
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
126143
- import { join as join17 } from "path";
126593
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync10 } from "fs";
126594
+ import { join as join18 } from "path";
126144
126595
  var MANAGED_MARKER = "<!-- ametyst-managed: sync-skills -->";
126145
126596
  var GITIGNORE_HEADER = "# ametyst-managed: sync-skills \u2014 pointer skills generated for this account; never commit them.";
126146
126597
  var GITIGNORE_HEADER_2 = "# Maintained by `ametyst serve` on every boot. Real skills without the managed marker are not listed.";
@@ -126184,22 +126635,22 @@ ${taskRunSection(slug, kind)}`;
126184
126635
  }
126185
126636
  function isManaged(file) {
126186
126637
  try {
126187
- return readFileSync13(file, "utf8").includes(MANAGED_MARKER);
126638
+ return readFileSync14(file, "utf8").includes(MANAGED_MARKER);
126188
126639
  } catch {
126189
126640
  return false;
126190
126641
  }
126191
126642
  }
126192
126643
  function listManagedDirs(root2) {
126193
- return readdirSync4(root2, { withFileTypes: true }).filter((e) => e.isDirectory() && isManaged(join17(root2, e.name, "SKILL.md"))).map((e) => e.name).sort();
126644
+ return readdirSync5(root2, { withFileTypes: true }).filter((e) => e.isDirectory() && isManaged(join18(root2, e.name, "SKILL.md"))).map((e) => e.name).sort();
126194
126645
  }
126195
126646
  function buildGitignoreContent(managedSlugs) {
126196
126647
  return [GITIGNORE_HEADER, GITIGNORE_HEADER_2, ".gitignore", ...managedSlugs.map((s) => `/${s}/`)].join("\n") + "\n";
126197
126648
  }
126198
126649
  function maintainGitignore(root2) {
126199
- const file = join17(root2, ".gitignore");
126650
+ const file = join18(root2, ".gitignore");
126200
126651
  const managed = listManagedDirs(root2);
126201
- if (existsSync13(file)) {
126202
- const current = readFileSync13(file, "utf8");
126652
+ if (existsSync14(file)) {
126653
+ const current = readFileSync14(file, "utf8");
126203
126654
  const firstLine = current.split(/\r?\n/, 1)[0];
126204
126655
  if (firstLine !== GITIGNORE_HEADER) {
126205
126656
  console.error(
@@ -126257,26 +126708,26 @@ async function syncSkills(opts = {}) {
126257
126708
  if (!desired.has(dir)) desired.set(dir, item);
126258
126709
  }
126259
126710
  const root2 = skillsRoot(target, global2);
126260
- mkdirSync8(root2, { recursive: true });
126711
+ mkdirSync9(root2, { recursive: true });
126261
126712
  let written = 0;
126262
126713
  const skipped = [];
126263
126714
  for (const [dir, item] of desired) {
126264
- const file = join17(dir, "SKILL.md");
126265
- if (existsSync13(file) && !isManaged(file)) {
126715
+ const file = join18(dir, "SKILL.md");
126716
+ if (existsSync14(file) && !isManaged(file)) {
126266
126717
  skipped.push(item.slug);
126267
126718
  console.warn(`\u26A0\uFE0F sync-skills: skipping "${item.slug}" \u2014 an unmanaged skill already exists at ${file}`);
126268
126719
  continue;
126269
126720
  }
126270
- mkdirSync8(dir, { recursive: true });
126721
+ mkdirSync9(dir, { recursive: true });
126271
126722
  writeFileSync10(file, buildStubContent(item));
126272
126723
  written++;
126273
126724
  }
126274
126725
  let pruned = 0;
126275
- for (const entry of readdirSync4(root2, { withFileTypes: true })) {
126726
+ for (const entry of readdirSync5(root2, { withFileTypes: true })) {
126276
126727
  if (!entry.isDirectory()) continue;
126277
- const dir = join17(root2, entry.name);
126728
+ const dir = join18(root2, entry.name);
126278
126729
  if (desired.has(dir)) continue;
126279
- if (!isManaged(join17(dir, "SKILL.md"))) continue;
126730
+ if (!isManaged(join18(dir, "SKILL.md"))) continue;
126280
126731
  rmSync(dir, { recursive: true, force: true });
126281
126732
  pruned++;
126282
126733
  }
@@ -126290,12 +126741,12 @@ function isSkillsAutosyncEnabled(env = process.env) {
126290
126741
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
126291
126742
  }
126292
126743
  function detectPresentTargets(deps = {}) {
126293
- const exists = deps.existsSync ?? existsSync14;
126744
+ const exists = deps.existsSync ?? existsSync15;
126294
126745
  const cwd = deps.cwd ?? (() => process.cwd());
126295
126746
  const home = deps.homedir ?? homedir12;
126296
126747
  const targets = [];
126297
- if (exists(join18(cwd(), ".claude")) || exists(join18(home(), ".claude"))) targets.push("claude");
126298
- if (exists(join18(cwd(), ".codex")) || exists(join18(home(), ".codex", "config.toml"))) targets.push("codex");
126748
+ if (exists(join19(cwd(), ".claude")) || exists(join19(home(), ".claude"))) targets.push("claude");
126749
+ if (exists(join19(cwd(), ".codex")) || exists(join19(home(), ".codex", "config.toml"))) targets.push("codex");
126299
126750
  return targets;
126300
126751
  }
126301
126752
  function resolveAutosyncTargets(clientName, deps = {}) {
@@ -126985,14 +127436,14 @@ init_esm_shims();
126985
127436
  // src/loops/materialize.ts
126986
127437
  init_esm_shims();
126987
127438
  init_paths();
126988
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
126989
- import { join as join19 } from "path";
127439
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
127440
+ import { join as join20 } from "path";
126990
127441
  var MIRRORED_DEFINITION_FILES = ["SKILL.md", "VISION.md", "README.md"];
126991
127442
  function materialize(loop2, fireId, stateDocs = []) {
126992
127443
  const dir = loopFireDir(loop2.slug, fireId);
126993
- mkdirSync9(dir, { recursive: true, mode: 448 });
127444
+ mkdirSync10(dir, { recursive: true, mode: 448 });
126994
127445
  for (const doc of stateDocs) {
126995
- writeFileSync11(join19(dir, doc.filename), doc.body, { mode: 384 });
127446
+ writeFileSync11(join20(dir, doc.filename), doc.body, { mode: 384 });
126996
127447
  }
126997
127448
  const files = {
126998
127449
  "SKILL.md": loop2.markdownBody ?? "",
@@ -127007,10 +127458,10 @@ function materialize(loop2, fireId, stateDocs = []) {
127007
127458
  files["dashboard.manifest.json"] = loop2.dashboardManifest;
127008
127459
  }
127009
127460
  for (const [name, body] of Object.entries(files)) {
127010
- writeFileSync11(join19(dir, name), body, { mode: 384 });
127461
+ writeFileSync11(join20(dir, name), body, { mode: 384 });
127011
127462
  }
127012
127463
  writeFileSync11(
127013
- join19(dir, "STATUS.md"),
127464
+ join20(dir, "STATUS.md"),
127014
127465
  `# STATUS \u2014 ${loop2.slug}
127015
127466
 
127016
127467
  loop_id: ${loop2.id}
@@ -127026,11 +127477,11 @@ queue: not started
127026
127477
  function mirrorDefinitionFiles(slug, files) {
127027
127478
  try {
127028
127479
  const root2 = loopDir(slug);
127029
- mkdirSync9(root2, { recursive: true, mode: 448 });
127480
+ mkdirSync10(root2, { recursive: true, mode: 448 });
127030
127481
  for (const name of [...MIRRORED_DEFINITION_FILES, "dashboard.html", "dashboard.manifest.json"]) {
127031
127482
  const body = files[name];
127032
127483
  if (body === void 0) continue;
127033
- writeFileSync11(join19(root2, name), body, { mode: 384 });
127484
+ writeFileSync11(join20(root2, name), body, { mode: 384 });
127034
127485
  }
127035
127486
  } catch {
127036
127487
  }
@@ -127156,15 +127607,15 @@ function resolveMaxBudgetUsd(opts, env = process.env) {
127156
127607
  // src/loops/heartbeat.ts
127157
127608
  init_esm_shims();
127158
127609
  import * as realFs2 from "fs";
127159
- import { join as join20 } from "path";
127610
+ import { join as join21 } from "path";
127160
127611
  function startHeartbeat(loopDir2, info, deps = {}) {
127161
127612
  const fs = deps.fs ?? realFs2;
127162
127613
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
127163
127614
  const setI = deps.setInterval ?? globalThis.setInterval;
127164
127615
  const clearI = deps.clearInterval ?? globalThis.clearInterval;
127165
127616
  const intervalMs = deps.intervalMs ?? 6e4;
127166
- const stateDir = join20(loopDir2, ".state");
127167
- const path2 = join20(stateDir, "fire.running");
127617
+ const stateDir = join21(loopDir2, ".state");
127618
+ const path2 = join21(stateDir, "fire.running");
127168
127619
  try {
127169
127620
  fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
127170
127621
  fs.writeFileSync(
@@ -127204,10 +127655,10 @@ ${info.sessionId ? `session=${info.sessionId}
127204
127655
  init_esm_shims();
127205
127656
  import * as realFs3 from "fs";
127206
127657
  import { homedir as homedir13 } from "os";
127207
- import { join as join21 } from "path";
127658
+ import { join as join22 } from "path";
127208
127659
  function transcriptPathFor(cwd, sessionId2, home = homedir13()) {
127209
127660
  const slug = cwd.replace(/\//g, "-");
127210
- return join21(home, ".claude", "projects", slug, `${sessionId2}.jsonl`);
127661
+ return join22(home, ".claude", "projects", slug, `${sessionId2}.jsonl`);
127211
127662
  }
127212
127663
  function parseTranscriptStats(jsonl) {
127213
127664
  const tokens = {
@@ -127278,9 +127729,9 @@ function recordFireAccounting(args) {
127278
127729
  }
127279
127730
  }
127280
127731
  function appendFireLine(fs, loopDir2, rec) {
127281
- const stateDir = join21(loopDir2, ".state");
127732
+ const stateDir = join22(loopDir2, ".state");
127282
127733
  fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
127283
- fs.appendFileSync(join21(stateDir, "fires.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 });
127734
+ fs.appendFileSync(join22(stateDir, "fires.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 });
127284
127735
  }
127285
127736
  function recordFailedLaunch(args) {
127286
127737
  const fs = args.deps?.fs ?? realFs3;
@@ -127309,8 +127760,8 @@ function recordFailedLaunch(args) {
127309
127760
  init_esm_shims();
127310
127761
  import { spawn as spawn2 } from "child_process";
127311
127762
  import { randomUUID as randomUUID5 } from "crypto";
127312
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
127313
- import { dirname as dirname8, join as join23 } from "path";
127763
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
127764
+ import { dirname as dirname8, join as join24 } from "path";
127314
127765
 
127315
127766
  // src/loops/claude-binary.ts
127316
127767
  init_esm_shims();
@@ -127362,7 +127813,7 @@ function ensureClaudeBinary(deps = {}) {
127362
127813
  // src/loops/concurrency.ts
127363
127814
  init_esm_shims();
127364
127815
  import * as realFs4 from "fs";
127365
- import { join as join22 } from "path";
127816
+ import { join as join23 } from "path";
127366
127817
  var DEFAULT_MAX_CONCURRENT_FIRES = 6;
127367
127818
  var DEFAULT_HEARTBEAT_STALE_MS = 10 * 6e4;
127368
127819
  function pidIsAlive(pid) {
@@ -127386,14 +127837,14 @@ function liveFires(loopRoot, deps = {}) {
127386
127837
  const staleMs = deps.staleMs ?? DEFAULT_HEARTBEAT_STALE_MS;
127387
127838
  let entries;
127388
127839
  try {
127389
- entries = fs.readdirSync(join22(loopRoot, "fires"));
127840
+ entries = fs.readdirSync(join23(loopRoot, "fires"));
127390
127841
  } catch {
127391
127842
  return [];
127392
127843
  }
127393
127844
  const out = [];
127394
127845
  for (const entry of entries) {
127395
127846
  try {
127396
- const beat = join22(loopRoot, "fires", String(entry), ".state", "fire.running");
127847
+ const beat = join23(loopRoot, "fires", String(entry), ".state", "fire.running");
127397
127848
  const st = fs.statSync(beat);
127398
127849
  const heartbeatAgeMs = now() - Number(st.mtimeMs);
127399
127850
  if (!(heartbeatAgeMs <= staleMs)) continue;
@@ -127421,7 +127872,7 @@ var SHIPBACK_SIGNAL_TIMEOUT_MS = 8e3;
127421
127872
  function preserveRefusedConstraints(slug, fireId, body) {
127422
127873
  try {
127423
127874
  const path2 = refusedConstraintsPath(slug, fireId);
127424
- mkdirSync10(dirname8(path2), { recursive: true, mode: 448 });
127875
+ mkdirSync11(dirname8(path2), { recursive: true, mode: 448 });
127425
127876
  writeFileSync12(path2, body, { mode: 384 });
127426
127877
  return { path: path2 };
127427
127878
  } catch (err) {
@@ -127500,8 +127951,8 @@ async function runLoop(loopId, opts = {}) {
127500
127951
  console.log(
127501
127952
  `Loop ${loop2.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
127502
127953
  );
127503
- const statusPath = join23(dir, "STATUS.md");
127504
- const statusBefore = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
127954
+ const statusPath = join24(dir, "STATUS.md");
127955
+ const statusBefore = existsSync16(statusPath) ? readFileSync15(statusPath, "utf-8") : "";
127505
127956
  writeFileSync12(
127506
127957
  statusPath,
127507
127958
  statusBefore.replace(
@@ -127560,8 +128011,8 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127560
128011
  }
127561
128012
  async function shipConstraints() {
127562
128013
  try {
127563
- const constraintsPath = join23(dir, "CONSTRAINTS.md");
127564
- const materializedConstraints = existsSync15(constraintsPath) ? readFileSync14(constraintsPath, "utf-8") : void 0;
128014
+ const constraintsPath = join24(dir, "CONSTRAINTS.md");
128015
+ const materializedConstraints = existsSync16(constraintsPath) ? readFileSync15(constraintsPath, "utf-8") : void 0;
127565
128016
  if (materializedConstraints === void 0) return;
127566
128017
  const boot = loop2.constraintsMd ?? "";
127567
128018
  for (let attempt = 1; attempt <= 2; attempt++) {
@@ -127636,7 +128087,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127636
128087
  void Promise.race([shipBackConstraints().then(() => "shipped"), deadline]).then((outcome) => {
127637
128088
  if (outcome === "timeout") {
127638
128089
  console.error(
127639
- `Loop ${loop2.slug}: the ship-back did not finish within ${SHIPBACK_SIGNAL_TIMEOUT_MS}ms of the signal \u2014 exiting anyway so the shutdown is not held open. Nothing was discarded: this fire's rules remain at ${join23(dir, "CONSTRAINTS.md")}, and any state doc that did not get shipped remains beside it in ${dir}. The next fire can recover them by hand from there \u2014 nothing in the CLI reads a prior fire's folder automatically. \u26D4 Constraints are shipped FIRST, so the state docs are the likelier victim of this deadline \u2014 though a constraints ship-back that alone exceeds it truncates the rules too.`
128090
+ `Loop ${loop2.slug}: the ship-back did not finish within ${SHIPBACK_SIGNAL_TIMEOUT_MS}ms of the signal \u2014 exiting anyway so the shutdown is not held open. Nothing was discarded: this fire's rules remain at ${join24(dir, "CONSTRAINTS.md")}, and any state doc that did not get shipped remains beside it in ${dir}. The next fire can recover them by hand from there \u2014 nothing in the CLI reads a prior fire's folder automatically. \u26D4 Constraints are shipped FIRST, so the state docs are the likelier victim of this deadline \u2014 though a constraints ship-back that alone exceeds it truncates the rules too.`
127640
128091
  );
127641
128092
  }
127642
128093
  }).finally(() => process.exit(130));
@@ -127672,7 +128123,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127672
128123
  startedAtEpochMs
127673
128124
  });
127674
128125
  }
127675
- const statusAfter = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
128126
+ const statusAfter = existsSync16(statusPath) ? readFileSync15(statusPath, "utf-8") : "";
127676
128127
  const clean2 = exitCode === 0 && /(vision\s*done|queue\s*drained|status:\s*done)/i.test(statusAfter) && !/(brake|crash|crashed|errored)/i.test(statusAfter);
127677
128128
  await shipBackConstraints();
127678
128129
  process.off("SIGINT", onSignal);
@@ -127710,9 +128161,9 @@ async function showLoop(loopId) {
127710
128161
  // src/loops/schedule.ts
127711
128162
  init_esm_shims();
127712
128163
  init_paths();
127713
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync11, rmSync as rmSync3, existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync15, accessSync as accessSync2, constants as constants2 } from "fs";
128164
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, rmSync as rmSync3, existsSync as existsSync17, readdirSync as readdirSync6, readFileSync as readFileSync16, accessSync as accessSync2, constants as constants2 } from "fs";
127714
128165
  import { execFileSync as execFileSync4 } from "child_process";
127715
- import { join as join24, dirname as dirname9 } from "path";
128166
+ import { join as join25, dirname as dirname9 } from "path";
127716
128167
  import { homedir as homedir14 } from "os";
127717
128168
  var LOOP_KIND = {
127718
128169
  labelPrefix: "xyz.ametyst.loop.",
@@ -127723,7 +128174,7 @@ var LOOP_KIND = {
127723
128174
  return args;
127724
128175
  },
127725
128176
  stateDir(slug, cwd) {
127726
- return join24(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
128177
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
127727
128178
  }
127728
128179
  };
127729
128180
  var TASK_KIND = {
@@ -127735,7 +128186,7 @@ var TASK_KIND = {
127735
128186
  return args;
127736
128187
  },
127737
128188
  stateDir(slug, cwd) {
127738
- return join24(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
128189
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
127739
128190
  }
127740
128191
  };
127741
128192
  var COMPOUND_KIND = {
@@ -127747,7 +128198,7 @@ var COMPOUND_KIND = {
127747
128198
  return args;
127748
128199
  },
127749
128200
  stateDir(slug, cwd) {
127750
- return join24(cwd, `.ametyst${ENV_SUFFIX}`, "compounds", safeSlug2(slug, this.noun), ".state");
128201
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, "compounds", safeSlug2(slug, this.noun), ".state");
127751
128202
  }
127752
128203
  };
127753
128204
  function safeSlug2(slug, noun) {
@@ -127774,7 +128225,7 @@ function parseAt(at) {
127774
128225
  return { hour, minute };
127775
128226
  }
127776
128227
  function plistPath(kind, home, slug) {
127777
- return join24(home, "Library", "LaunchAgents", `${label(kind, slug)}.plist`);
128228
+ return join25(home, "Library", "LaunchAgents", `${label(kind, slug)}.plist`);
127778
128229
  }
127779
128230
  function launchctl(args) {
127780
128231
  try {
@@ -127790,7 +128241,7 @@ function isLoaded(lbl) {
127790
128241
  }
127791
128242
  function readInteractiveDefaultModel(home) {
127792
128243
  try {
127793
- const raw = readFileSync15(join24(home, ".claude", "settings.json"), "utf-8");
128244
+ const raw = readFileSync16(join25(home, ".claude", "settings.json"), "utf-8");
127794
128245
  const model = JSON.parse(String(raw)).model;
127795
128246
  return typeof model === "string" && model.trim() ? model.trim() : void 0;
127796
128247
  } catch {
@@ -127892,9 +128343,9 @@ ${progArgs}
127892
128343
  ${envEntries}
127893
128344
  </dict>
127894
128345
  <key>StandardOutPath</key>
127895
- <string>${escapeXml(join24(stateDir, LAUNCHD_OUT))}</string>
128346
+ <string>${escapeXml(join25(stateDir, LAUNCHD_OUT))}</string>
127896
128347
  <key>StandardErrorPath</key>
127897
- <string>${escapeXml(join24(stateDir, LAUNCHD_ERR))}</string>
128348
+ <string>${escapeXml(join25(stateDir, LAUNCHD_ERR))}</string>
127898
128349
  <key>AbandonProcessGroup</key>
127899
128350
  <true/>
127900
128351
  ${scheduleBlock}
@@ -127960,8 +128411,8 @@ function schedule(kind, slug, opts = {}) {
127960
128411
  const path2 = plistPath(kind, home, slug);
127961
128412
  const stateDir2 = kind.stateDir(slug, cwd);
127962
128413
  assertWritable(cwd, "working directory");
127963
- mkdirSync11(dirname9(path2), { recursive: true });
127964
- mkdirSync11(stateDir2, { recursive: true });
128414
+ mkdirSync12(dirname9(path2), { recursive: true });
128415
+ mkdirSync12(stateDir2, { recursive: true });
127965
128416
  assertWritable(stateDir2, "log directory");
127966
128417
  writeFileSync13(path2, plistXml(lbl, args, scheduleBlock, cwd, jobEnv, stateDir2), { mode: 384 });
127967
128418
  if (isLoaded(lbl)) {
@@ -127984,7 +128435,7 @@ launchctl said: ${loaded.output.trim()}` : "")
127984
128435
  }
127985
128436
  const stateDir = kind.stateDir(slug, cwd);
127986
128437
  assertWritable(cwd, "working directory");
127987
- mkdirSync11(stateDir, { recursive: true });
128438
+ mkdirSync12(stateDir, { recursive: true });
127988
128439
  assertWritable(stateDir, "log directory");
127989
128440
  const cronEnv = { PATH: envPath };
127990
128441
  if (model) cronEnv.ANTHROPIC_MODEL = model;
@@ -128017,9 +128468,9 @@ function list(kind, opts = {}) {
128017
128468
  const home = opts.home ?? homedir14();
128018
128469
  const prefix = kind.labelPrefix;
128019
128470
  if (platform === "darwin") {
128020
- const dir = join24(home, "Library", "LaunchAgents");
128021
- if (!existsSync16(dir)) return [];
128022
- return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
128471
+ const dir = join25(home, "Library", "LaunchAgents");
128472
+ if (!existsSync17(dir)) return [];
128473
+ return readdirSync6(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
128023
128474
  }
128024
128475
  return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length));
128025
128476
  }
@@ -128079,13 +128530,13 @@ function listEntries(kind, opts = {}) {
128079
128530
  const home = opts.home ?? homedir14();
128080
128531
  const prefix = kind.labelPrefix;
128081
128532
  if (platform === "darwin") {
128082
- const dir = join24(home, "Library", "LaunchAgents");
128083
- if (!existsSync16(dir)) return [];
128084
- return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
128533
+ const dir = join25(home, "Library", "LaunchAgents");
128534
+ if (!existsSync17(dir)) return [];
128535
+ return readdirSync6(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
128085
128536
  const slug = f.slice(prefix.length, -".plist".length);
128086
128537
  let envKeys = [];
128087
128538
  try {
128088
- envKeys = plistEnvKeys(String(readFileSync15(join24(dir, f), "utf-8")));
128539
+ envKeys = plistEnvKeys(String(readFileSync16(join25(dir, f), "utf-8")));
128089
128540
  } catch {
128090
128541
  envKeys = [];
128091
128542
  }