@ametyst/cli 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +725 -178
  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,236 @@ 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
+ MissingGrantKeyError: () => MissingGrantKeyError,
112030
+ buildGrantVault: () => buildGrantVault,
112031
+ createGrantKeyIssuer: () => createGrantKeyIssuer,
112032
+ deleteGrantEntry: () => deleteGrantEntry,
112033
+ ensureGrantsDir: () => ensureGrantsDir,
112034
+ generateGrantKey: () => generateGrantKey,
112035
+ grantEntryPath: () => grantEntryPath,
112036
+ grantsDir: () => grantsDir,
112037
+ hasGrantSigningKey: () => hasGrantSigningKey,
112038
+ isMissingGrantKeyError: () => isMissingGrantKeyError,
112039
+ listGrantAddresses: () => listGrantAddresses,
112040
+ listGrantEntries: () => listGrantEntries,
112041
+ missingGrantKeyMessage: () => missingGrantKeyMessage,
112042
+ normalizeGrantAddress: () => normalizeGrantAddress,
112043
+ parseGrantEntry: () => parseGrantEntry,
112044
+ persistGrantKey: () => persistGrantKey,
112045
+ readGrantEntry: () => readGrantEntry,
112046
+ recordGrantRowId: () => recordGrantRowId,
112047
+ resolveGrantSigningKey: () => resolveGrantSigningKey
112048
+ });
112049
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readdirSync as readdirSync4, unlinkSync as unlinkSync5 } from "fs";
112050
+ import { join as join6 } from "path";
112051
+ function grantsDir() {
112052
+ return join6(WALLETS_DIR, "grants");
112053
+ }
112054
+ function resolveDir(dir) {
112055
+ return dir ?? grantsDir();
112056
+ }
112057
+ function ensureGrantsDir(dir) {
112058
+ const target = resolveDir(dir);
112059
+ if (target === grantsDir()) ensureDirectories();
112060
+ if (!existsSync7(target)) mkdirSync6(target, { mode: 448, recursive: true });
112061
+ return target;
112062
+ }
112063
+ function normalizeGrantAddress(address) {
112064
+ if (typeof address !== "string" || !ADDRESS_RE2.test(address)) {
112065
+ throw new Error(`Not a 0x-hex EOA address: ${JSON.stringify(address)}`);
112066
+ }
112067
+ return address.toLowerCase();
112068
+ }
112069
+ function grantEntryPath(address, dir) {
112070
+ return join6(resolveDir(dir), `${normalizeGrantAddress(address)}.json`);
112071
+ }
112072
+ function generateGrantKey() {
112073
+ const derived = derivePlaintextWallet(generatePrivateKey());
112074
+ return { address: derived.address.toLowerCase(), privateKey: derived.privateKey };
112075
+ }
112076
+ function buildGrantVault(address, privateKey, options) {
112077
+ if (options.encrypted) {
112078
+ if (!options.passphrase) {
112079
+ throw new Error(
112080
+ "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."
112081
+ );
112082
+ }
112083
+ return JSON.parse(importWallet(privateKey, options.passphrase));
112084
+ }
112085
+ return JSON.parse(serializePlaintextVault(address, privateKey));
112086
+ }
112087
+ function persistGrantKey(params, dir) {
112088
+ const address = normalizeGrantAddress(params.address);
112089
+ const target = ensureGrantsDir(dir);
112090
+ const entry = {
112091
+ format: GRANT_ENTRY_FORMAT,
112092
+ address,
112093
+ createdAt: params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
112094
+ workspaceId: params.workspaceId ?? null,
112095
+ policyId: params.policyId ?? null,
112096
+ backendWalletRowId: params.backendWalletRowId ?? null,
112097
+ vault: buildGrantVault(address, params.privateKey, {
112098
+ encrypted: params.encrypted,
112099
+ passphrase: params.passphrase
112100
+ })
112101
+ };
112102
+ writeSecretFileAtomic(join6(target, `${address}.json`), JSON.stringify(entry, null, 2));
112103
+ return entry;
112104
+ }
112105
+ function parseGrantEntry(json) {
112106
+ let parsed;
112107
+ try {
112108
+ parsed = JSON.parse(json);
112109
+ } catch {
112110
+ return null;
112111
+ }
112112
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
112113
+ const p = parsed;
112114
+ if (p.format !== GRANT_ENTRY_FORMAT) return null;
112115
+ if (typeof p.address !== "string" || !ADDRESS_RE2.test(p.address)) return null;
112116
+ if (p.vault === null || typeof p.vault !== "object" || Array.isArray(p.vault)) return null;
112117
+ return {
112118
+ format: GRANT_ENTRY_FORMAT,
112119
+ address: p.address.toLowerCase(),
112120
+ createdAt: typeof p.createdAt === "string" ? p.createdAt : "",
112121
+ workspaceId: typeof p.workspaceId === "string" ? p.workspaceId : null,
112122
+ policyId: typeof p.policyId === "number" ? p.policyId : null,
112123
+ backendWalletRowId: typeof p.backendWalletRowId === "string" ? p.backendWalletRowId : null,
112124
+ vault: p.vault
112125
+ };
112126
+ }
112127
+ function readGrantEntry(address, dir) {
112128
+ let path2;
112129
+ try {
112130
+ path2 = grantEntryPath(address, dir);
112131
+ } catch {
112132
+ return null;
112133
+ }
112134
+ try {
112135
+ return parseGrantEntry(readFileSync7(path2, "utf-8"));
112136
+ } catch {
112137
+ return null;
112138
+ }
112139
+ }
112140
+ function listGrantEntries(dir) {
112141
+ const target = resolveDir(dir);
112142
+ let names;
112143
+ try {
112144
+ names = readdirSync4(target);
112145
+ } catch {
112146
+ return [];
112147
+ }
112148
+ const entries = [];
112149
+ for (const name of names) {
112150
+ if (!name.endsWith(".json")) continue;
112151
+ let raw;
112152
+ try {
112153
+ raw = readFileSync7(join6(target, name), "utf-8");
112154
+ } catch {
112155
+ continue;
112156
+ }
112157
+ const entry = parseGrantEntry(raw);
112158
+ if (entry) entries.push(entry);
112159
+ }
112160
+ return entries;
112161
+ }
112162
+ function listGrantAddresses(options = {}, dir) {
112163
+ const active = options.workspaceId ?? null;
112164
+ return listGrantEntries(dir).filter((e) => active === null || e.workspaceId === null || e.workspaceId === active).map((e) => e.address);
112165
+ }
112166
+ function recordGrantRowId(address, rowId, dir) {
112167
+ const entry = readGrantEntry(address, dir);
112168
+ if (!entry) return false;
112169
+ try {
112170
+ const next = { ...entry, backendWalletRowId: rowId ?? null };
112171
+ writeSecretFileAtomic(grantEntryPath(address, dir), JSON.stringify(next, null, 2));
112172
+ return true;
112173
+ } catch {
112174
+ return false;
112175
+ }
112176
+ }
112177
+ function deleteGrantEntry(address, dir) {
112178
+ try {
112179
+ unlinkSync5(grantEntryPath(address, dir));
112180
+ } catch {
112181
+ }
112182
+ }
112183
+ function missingGrantKeyMessage(address) {
112184
+ return `no local key for grant address ${address} \u2014 this machine cannot sign for that grant. Run requestAccess again to mint a fresh session key and have your admin approve it.`;
112185
+ }
112186
+ function isMissingGrantKeyError(err) {
112187
+ return err instanceof MissingGrantKeyError;
112188
+ }
112189
+ function hasGrantSigningKey(params) {
112190
+ let address;
112191
+ try {
112192
+ address = normalizeGrantAddress(params.address);
112193
+ } catch {
112194
+ return false;
112195
+ }
112196
+ if (readGrantEntry(address, params.dir)) return true;
112197
+ return typeof params.vaultAddress === "string" && params.vaultAddress.toLowerCase() === address;
112198
+ }
112199
+ function resolveGrantSigningKey(params) {
112200
+ const address = normalizeGrantAddress(params.address);
112201
+ const entry = readGrantEntry(address, params.dir);
112202
+ if (entry) {
112203
+ return readVaultPrivateKey(JSON.stringify(entry.vault), params.passphrase);
112204
+ }
112205
+ const vaultAddress = params.vaultAddress;
112206
+ if (params.vaultJson && typeof vaultAddress === "string" && vaultAddress.toLowerCase() === address) {
112207
+ return readVaultPrivateKey(params.vaultJson, params.passphrase);
112208
+ }
112209
+ throw new MissingGrantKeyError(address);
112210
+ }
112211
+ function createGrantKeyIssuer(options) {
112212
+ return {
112213
+ issue({ policyId }) {
112214
+ const { address, privateKey } = generateGrantKey();
112215
+ persistGrantKey(
112216
+ {
112217
+ address,
112218
+ privateKey,
112219
+ encrypted: options.encrypted,
112220
+ passphrase: options.passphrase,
112221
+ workspaceId: options.workspaceId ?? null,
112222
+ policyId
112223
+ },
112224
+ options.dir
112225
+ );
112226
+ return address;
112227
+ },
112228
+ recordRow(address, rowId) {
112229
+ recordGrantRowId(address, rowId, options.dir);
112230
+ }
112231
+ };
112232
+ }
112233
+ var GRANT_ENTRY_FORMAT, ADDRESS_RE2, MissingGrantKeyError;
112234
+ var init_grant_keystore = __esm({
112235
+ "src/wallet/grant-keystore.ts"() {
112236
+ "use strict";
112237
+ init_esm_shims();
112238
+ init_accounts();
112239
+ init_paths();
112240
+ init_core();
112241
+ init_plaintext_vault();
112242
+ GRANT_ENTRY_FORMAT = "grant-key-v1";
112243
+ ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
112244
+ MissingGrantKeyError = class extends Error {
112245
+ grantAddress;
112246
+ constructor(address) {
112247
+ super(missingGrantKeyMessage(address));
112248
+ this.name = "MissingGrantKeyError";
112249
+ this.grantAddress = address;
112250
+ }
112251
+ };
112252
+ }
112253
+ });
112254
+
112016
112255
  // src/commands/init-claim.ts
112017
112256
  var init_claim_exports = {};
112018
112257
  __export(init_claim_exports, {
@@ -112026,7 +112265,7 @@ __export(init_claim_exports, {
112026
112265
  vaultBackupPath: () => vaultBackupPath,
112027
112266
  vaultBackupSuffix: () => vaultBackupSuffix
112028
112267
  });
112029
- import { chmodSync as chmodSync3, constants as fsConstants, copyFileSync as copyFileSync3, existsSync as existsSync7 } from "fs";
112268
+ import { chmodSync as chmodSync3, constants as fsConstants, copyFileSync as copyFileSync3, existsSync as existsSync8 } from "fs";
112030
112269
  import { createDecipheriv as createDecipheriv2 } from "crypto";
112031
112270
  function claimErrorMessage(failure) {
112032
112271
  switch (failure.kind) {
@@ -112169,7 +112408,7 @@ function vaultBackupPath(vaultPath, at) {
112169
112408
  return `${vaultPath}.bak-${vaultBackupSuffix(at)}`;
112170
112409
  }
112171
112410
  function backupVaultFile(vaultPath, at = /* @__PURE__ */ new Date()) {
112172
- if (!existsSync7(vaultPath)) return null;
112411
+ if (!existsSync8(vaultPath)) return null;
112173
112412
  const target = vaultBackupPath(vaultPath, at);
112174
112413
  try {
112175
112414
  copyFileSync3(vaultPath, target, fsConstants.COPYFILE_EXCL);
@@ -112197,13 +112436,13 @@ var init_version5 = __esm({
112197
112436
  "src/version.ts"() {
112198
112437
  "use strict";
112199
112438
  init_esm_shims();
112200
- CLI_VERSION = true ? "0.3.5" : "0.0.0-dev";
112439
+ CLI_VERSION = true ? "0.3.7" : "0.0.0-dev";
112201
112440
  }
112202
112441
  });
112203
112442
 
112204
112443
  // src/connections/catalog.ts
112205
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
112206
- import { join as join6 } from "path";
112444
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
112445
+ import { join as join7 } from "path";
112207
112446
  function declaredVerbsHeader() {
112208
112447
  return SUPPORTED_HTTP_METHODS.join(",");
112209
112448
  }
@@ -112459,8 +112698,8 @@ function parseConnectorsResponseDetailed(body, onWarn = warnToStderr) {
112459
112698
  function readCachedCatalogStatus() {
112460
112699
  let raw;
112461
112700
  try {
112462
- if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112463
- raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112701
+ if (!existsSync9(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112702
+ raw = JSON.parse(readFileSync8(CONNECTOR_CACHE_PATH, "utf-8"));
112464
112703
  } catch (e) {
112465
112704
  return { state: "unreadable", reason: e instanceof Error ? e.message : String(e) };
112466
112705
  }
@@ -112496,8 +112735,8 @@ function readMirrorRowsFromDisk() {
112496
112735
  }
112497
112736
  function readMirrorContainer() {
112498
112737
  try {
112499
- if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112500
- const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112738
+ if (!existsSync9(CONNECTOR_CACHE_PATH)) return { state: "absent" };
112739
+ const raw = JSON.parse(readFileSync8(CONNECTOR_CACHE_PATH, "utf-8"));
112501
112740
  if (!isRecord(raw)) {
112502
112741
  return { state: "unreadable", reason: "the mirror's top level is not an object" };
112503
112742
  }
@@ -112511,8 +112750,8 @@ function readMirrorContainer() {
112511
112750
  }
112512
112751
  function readMirrorFetchedAt() {
112513
112752
  try {
112514
- if (!existsSync8(CONNECTOR_CACHE_PATH)) return "";
112515
- const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
112753
+ if (!existsSync9(CONNECTOR_CACHE_PATH)) return "";
112754
+ const raw = JSON.parse(readFileSync8(CONNECTOR_CACHE_PATH, "utf-8"));
112516
112755
  if (!isRecord(raw)) return "";
112517
112756
  return typeof raw.fetchedAt === "string" ? raw.fetchedAt : "";
112518
112757
  } catch {
@@ -112763,7 +113002,7 @@ var init_catalog = __esm({
112763
113002
  SOURCE_OPERATORS = ["provider", "third_party", "ametyst"];
112764
113003
  SOURCE_PREFERENCE_RULES = ["official-mcp"];
112765
113004
  CONNECTOR_FEED_PATH = "/api/v1/virtual-wallets/capabilities/connectors";
112766
- CONNECTOR_CACHE_PATH = join6(AMETYST_DIR, "connectors.json");
113005
+ CONNECTOR_CACHE_PATH = join7(AMETYST_DIR, "connectors.json");
112767
113006
  CatalogUnavailableError = class extends Error {
112768
113007
  constructor(reason, message) {
112769
113008
  super(message);
@@ -113501,15 +113740,15 @@ var init_call2 = __esm({
113501
113740
  });
113502
113741
 
113503
113742
  // 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";
113743
+ import { chmodSync as chmodSync4, closeSync as closeSync2, existsSync as existsSync12, mkdirSync as mkdirSync8, openSync as openSync2 } from "fs";
113744
+ import { dirname as dirname7, join as join17 } from "path";
113506
113745
  function connectionsDbPath() {
113507
- return join16(AMETYST_DIR, CONNECTIONS_DB_FILENAME);
113746
+ return join17(AMETYST_DIR, CONNECTIONS_DB_FILENAME);
113508
113747
  }
113509
113748
  function ensureOwnerOnlyFile(path2) {
113510
113749
  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));
113750
+ mkdirSync8(dirname7(path2), { recursive: true, mode: DIR_MODE });
113751
+ if (!existsSync12(path2)) closeSync2(openSync2(path2, "a", CONNECTIONS_DB_FILE_MODE));
113513
113752
  chmodSync4(path2, CONNECTIONS_DB_FILE_MODE);
113514
113753
  }
113515
113754
  function sqliteWasmDriver(db, raw) {
@@ -114718,6 +114957,44 @@ function hostNudges(rows) {
114718
114957
 
114719
114958
  // src/commands/login.ts
114720
114959
  init_resolve();
114960
+
114961
+ // src/config/workspace-binding.ts
114962
+ init_esm_shims();
114963
+ init_config();
114964
+ function workspaceKeyFrom(profile) {
114965
+ const name = profile?.companyName;
114966
+ if (typeof name !== "string") return null;
114967
+ const trimmed = name.trim().toLowerCase();
114968
+ return trimmed === "" ? null : trimmed;
114969
+ }
114970
+ function isWorkspaceSwitch(previous, next) {
114971
+ if (previous === null || next === null) return false;
114972
+ return previous !== next;
114973
+ }
114974
+ function readBoundWorkspace() {
114975
+ const config = loadConfig();
114976
+ const key = config?.workspaceId;
114977
+ return typeof key === "string" && key.trim() !== "" ? key : null;
114978
+ }
114979
+ function recordBoundWorkspace(key, observedAt) {
114980
+ const config = loadConfig();
114981
+ if (!config) return false;
114982
+ const { apiKey: _injected, apiKeySource: _src, ...persisted } = config;
114983
+ saveConfig({ ...persisted, workspaceId: key, workspaceIdObservedAt: observedAt });
114984
+ return true;
114985
+ }
114986
+ function workspaceSwitchNotice(previous, next) {
114987
+ 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.`;
114988
+ }
114989
+ function applyObservedWorkspace(profile, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
114990
+ const key = workspaceKeyFrom(profile);
114991
+ const previous = readBoundWorkspace();
114992
+ const switched = isWorkspaceSwitch(previous, key);
114993
+ if (key !== null && key !== previous) recordBoundWorkspace(key, observedAt);
114994
+ return { key, previous, switched };
114995
+ }
114996
+
114997
+ // src/commands/login.ts
114721
114998
  function lastObservedStanceLine(config) {
114722
114999
  const observed = config?.requireWalletPassphrase;
114723
115000
  if (typeof observed !== "boolean") {
@@ -114779,6 +115056,10 @@ async function loginCommand(options) {
114779
115056
  if (observedStance !== void 0) {
114780
115057
  setRequireWalletPassphrase(observedStance, (/* @__PURE__ */ new Date()).toISOString());
114781
115058
  }
115059
+ const workspace = applyObservedWorkspace(profile);
115060
+ if (workspace.switched && workspace.previous && workspace.key) {
115061
+ console.log(workspaceSwitchNotice(workspace.previous, workspace.key));
115062
+ }
114782
115063
  const stance = resolveWalletPassphraseStance(observedStance, config.requireWalletPassphrase);
114783
115064
  const who = profile ? `Logged in as ${profile.employeeName} @ ${profile.companyName ?? "(no company)"}.` : "Logged in.";
114784
115065
  console.log(who);
@@ -114926,8 +115207,9 @@ async function fetchAvailablePolicies(client, apiKey) {
114926
115207
  const response = await client.getPoliciesFromVirtualWalletsManager(apiKey);
114927
115208
  return normalizePoliciesResponse(response);
114928
115209
  }
114929
- async function submitAccessRequest(client, apiKey, eoaAddress, policyId) {
114930
- return client.createVirtualWallet(
115210
+ async function submitRotatedAccessRequest(client, apiKey, policyId, issuer) {
115211
+ const eoaAddress = await issuer.issue({ policyId });
115212
+ const result = await client.createVirtualWallet(
114931
115213
  apiKey,
114932
115214
  CLI_WALLET_NAME,
114933
115215
  CLI_WALLET_URL,
@@ -114935,6 +115217,11 @@ async function submitAccessRequest(client, apiKey, eoaAddress, policyId) {
114935
115217
  policyId,
114936
115218
  eoaAddress
114937
115219
  );
115220
+ try {
115221
+ issuer.recordRow?.(eoaAddress, result?.id ? String(result.id) : void 0);
115222
+ } catch {
115223
+ }
115224
+ return { ...result, eoaAddress };
114938
115225
  }
114939
115226
  function pickDefaultPolicyIndex(policies) {
114940
115227
  const i = policies.findIndex(
@@ -115133,6 +115420,7 @@ async function freshWalletRecovery(apiKey, options) {
115133
115420
  }
115134
115421
  }
115135
115422
  let address;
115423
+ let vaultPassphrase;
115136
115424
  if (options.noPassphrase) {
115137
115425
  const wallet = createPlaintextWallet();
115138
115426
  persistPlaintextVault(wallet.address, wallet.privateKey);
@@ -115153,6 +115441,7 @@ New wallet created. Address: ${address}`);
115153
115441
  process.exit(1);
115154
115442
  return false;
115155
115443
  }
115444
+ vaultPassphrase = passphrase;
115156
115445
  const keystoreJson = createWallet(passphrase);
115157
115446
  persistVault(keystoreJson);
115158
115447
  address = getWalletAddress(keystoreJson);
@@ -115201,8 +115490,15 @@ Create one in the Ametyst web app (admin panel \u2192 policies), then re-run 'am
115201
115490
  process.exit(1);
115202
115491
  return false;
115203
115492
  }
115493
+ let grantAddress;
115204
115494
  try {
115205
- await submitAccessRequest(client, apiKey, address, policyId);
115495
+ const { createGrantKeyIssuer: createGrantKeyIssuer2 } = await Promise.resolve().then(() => (init_grant_keystore(), grant_keystore_exports));
115496
+ const submitted = await submitRotatedAccessRequest(client, apiKey, policyId, createGrantKeyIssuer2({
115497
+ encrypted: !options.noPassphrase,
115498
+ passphrase: vaultPassphrase,
115499
+ workspaceId: readBoundWorkspace()
115500
+ }));
115501
+ grantAddress = submitted.eoaAddress;
115206
115502
  } catch (e) {
115207
115503
  const msg = e instanceof Error ? e.message : String(e);
115208
115504
  console.error(`
@@ -115214,8 +115510,9 @@ Could not submit the authorization request (${msg}).`);
115214
115510
  const policyName = typeof chosen.name === "string" && chosen.name ? chosen.name : `policy ${policyId}`;
115215
115511
  console.log(`
115216
115512
  Authorization request sent \u2014 PENDING approval.
115217
- Wallet: ${address}
115218
- Policy: ${policyName} (id ${policyId})
115513
+ Wallet: ${address}
115514
+ Session key: ${grantAddress}
115515
+ Policy: ${policyName} (id ${policyId})
115219
115516
 
115220
115517
  Next step: approve the request in the Ametyst web app admin panel (check your
115221
115518
  notifications there). Once it's approved, you're done \u2014 run 'ametyst status' to
@@ -115437,14 +115734,19 @@ function isNewerGrant(candidate, incumbent) {
115437
115734
  }
115438
115735
  return candidateIdUsable && !incumbentIdUsable;
115439
115736
  }
115440
- function selectNewestApprovedWallet(wallets, eoaAddress) {
115737
+ function selectNewestApprovedWalletAmong(wallets, addresses) {
115441
115738
  if (!Array.isArray(wallets)) return void 0;
115442
- const eoa = nonEmptyLowerCase(eoaAddress);
115443
- if (!eoa) return void 0;
115739
+ const candidates = /* @__PURE__ */ new Set();
115740
+ for (const address of addresses ?? []) {
115741
+ const normalized = nonEmptyLowerCase(address);
115742
+ if (normalized) candidates.add(normalized);
115743
+ }
115744
+ if (candidates.size === 0) return void 0;
115444
115745
  let newest;
115445
115746
  for (const wallet of wallets) {
115446
115747
  if (wallet?.status !== "approved") continue;
115447
- if (nonEmptyLowerCase(wallet?.address) !== eoa) continue;
115748
+ const rowAddress = nonEmptyLowerCase(wallet?.address);
115749
+ if (!rowAddress || !candidates.has(rowAddress)) continue;
115448
115750
  if (newest === void 0 || isNewerGrant(wallet, newest)) newest = wallet;
115449
115751
  }
115450
115752
  return newest;
@@ -115454,7 +115756,60 @@ function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
115454
115756
  if (pendingWalletId) {
115455
115757
  return wallets.find((w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved");
115456
115758
  }
115457
- return selectNewestApprovedWallet(wallets, eoaAddress);
115759
+ return selectNewestApprovedWalletAmong(
115760
+ wallets,
115761
+ Array.isArray(eoaAddress) ? eoaAddress : eoaAddress === void 0 ? [] : [eoaAddress]
115762
+ );
115763
+ }
115764
+
115765
+ // src/mcp-server/signer-binding.ts
115766
+ init_esm_shims();
115767
+ var BINDING_DERIVED_KEYS = [
115768
+ "virtualWalletKernelAccountClient",
115769
+ "permissionPlugin",
115770
+ "kernelDomain",
115771
+ "policyPrecheckPassed"
115772
+ ];
115773
+ function nonEmpty(value2) {
115774
+ return typeof value2 === "string" && value2.trim() !== "" ? value2 : void 0;
115775
+ }
115776
+ function sameAddress(a, b) {
115777
+ return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
115778
+ }
115779
+ function signerBindingMoved(state, row) {
115780
+ const rowId = row?.id != null ? String(row.id) : void 0;
115781
+ const rowAddress = nonEmpty(row?.address);
115782
+ if (state.virtualWalletId && rowId && state.virtualWalletId !== rowId) return true;
115783
+ if (state.signerAddress && rowAddress && !sameAddress(state.signerAddress, rowAddress)) return true;
115784
+ return false;
115785
+ }
115786
+ function bindSignerToGrantRow(state, row) {
115787
+ const changed = signerBindingMoved(state, row);
115788
+ if (changed) {
115789
+ for (const key of BINDING_DERIVED_KEYS) delete state[key];
115790
+ }
115791
+ const rowAddress = nonEmpty(row?.address);
115792
+ if (rowAddress) state.signerAddress = rowAddress;
115793
+ const rowWalletAddress = nonEmpty(row?.walletAddress) ?? nonEmpty(row?.kernelAccountAddress);
115794
+ const rowPaymentManagerAddress = nonEmpty(row?.paymentManagerAddress);
115795
+ const rowPolicyId = row?.policyAssociated != null ? String(row.policyAssociated) : void 0;
115796
+ const rowId = row?.id != null ? String(row.id) : void 0;
115797
+ if (changed) {
115798
+ state.walletAddress = rowWalletAddress;
115799
+ state.paymentManagerAddress = rowPaymentManagerAddress;
115800
+ state.policyId = rowPolicyId;
115801
+ state.policyOnchainPermissions = nonEmpty(row?.policyOnchainPermissions);
115802
+ } else {
115803
+ state.walletAddress = rowWalletAddress ?? state.walletAddress;
115804
+ state.paymentManagerAddress = rowPaymentManagerAddress ?? state.paymentManagerAddress;
115805
+ state.policyId = rowPolicyId ?? state.policyId;
115806
+ state.policyOnchainPermissions = nonEmpty(row?.policyOnchainPermissions) ?? state.policyOnchainPermissions;
115807
+ }
115808
+ if (rowId) state.virtualWalletId = rowId;
115809
+ if (row?.policyName != null) state.policyName = String(row.policyName);
115810
+ if (row?.policyValidUntil != null) state.policyValidUntil = BigInt(row.policyValidUntil);
115811
+ state.authorizationStatus = "approved";
115812
+ return { changed, signerAddress: state.signerAddress };
115458
115813
  }
115459
115814
 
115460
115815
  // src/mcp-server/start-session.ts
@@ -115594,7 +115949,10 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115594
115949
  let activeWallet;
115595
115950
  let grantChanged = false;
115596
115951
  try {
115597
- activeWallet = selectNewestApprovedWallet(wallets, ctx.eoaAddress);
115952
+ activeWallet = selectNewestApprovedWalletAmong(wallets, [
115953
+ ctx.eoaAddress,
115954
+ ...ctx.candidateAddresses ?? []
115955
+ ]);
115598
115956
  if (activeWallet) {
115599
115957
  const newestId = String(activeWallet.id);
115600
115958
  const rowWalletAddress = activeWallet.walletAddress || activeWallet.kernelAccountAddress;
@@ -115614,6 +115972,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115614
115972
  policyId = rowPolicyId ?? policyId;
115615
115973
  }
115616
115974
  virtualWalletId = newestId;
115975
+ if (typeof activeWallet.address === "string" && activeWallet.address.trim() !== "") {
115976
+ credentials.signerAddress = activeWallet.address;
115977
+ }
115617
115978
  credentials.walletAddress = walletAddress;
115618
115979
  credentials.virtualWalletId = virtualWalletId;
115619
115980
  credentials.paymentManagerAddress = paymentManagerAddress;
@@ -115646,7 +116007,8 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
115646
116007
  if (walletAddress && virtualWalletId && paymentManagerAddress && policyId) {
115647
116008
  let privateKey = null;
115648
116009
  try {
115649
- privateKey = readVaultPrivateKey(ctx.walletKeystoreJson, ctx.passphrase);
116010
+ const signerAddress = credentials.signerAddress ?? ctx.eoaAddress;
116011
+ privateKey = ctx.resolveSigningKey && signerAddress ? ctx.resolveSigningKey(signerAddress) : readVaultPrivateKey(ctx.walletKeystoreJson, ctx.passphrase);
115650
116012
  const { virtualWalletsManagers: virtualWalletsManagers2, financialAccounts: financialAccounts2 } = ctx.sdk;
115651
116013
  const walletData = await virtualWalletsManagers2.getVirtualWalletDataByApiKey(
115652
116014
  ctx.apiKey,
@@ -115761,11 +116123,11 @@ function clearWalletScopedCredentials(target) {
115761
116123
  delete target[key];
115762
116124
  }
115763
116125
  }
115764
- function sameAddress(a, b) {
116126
+ function sameAddress2(a, b) {
115765
116127
  return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
115766
116128
  }
115767
116129
  function mergeStartSessionCredentials(target, patch, responseKey, options = {}) {
115768
- const walletChanged = patch.eoaAddress !== void 0 && !sameAddress(patch.eoaAddress, target.eoaAddress);
116130
+ const walletChanged = patch.eoaAddress !== void 0 && !sameAddress2(patch.eoaAddress, target.eoaAddress);
115769
116131
  if (walletChanged || options.grantChanged) {
115770
116132
  clearWalletScopedCredentials(target);
115771
116133
  }
@@ -116946,13 +117308,13 @@ init_local_state();
116946
117308
  // src/mcp-server/materialize-task.ts
116947
117309
  init_esm_shims();
116948
117310
  init_paths();
116949
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
116950
- import { join as join8 } from "path";
117311
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
117312
+ import { join as join9 } from "path";
116951
117313
 
116952
117314
  // src/loops/state-docs.ts
116953
117315
  init_esm_shims();
116954
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
116955
- import { join as join7 } from "path";
117316
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
117317
+ import { join as join8 } from "path";
116956
117318
 
116957
117319
  // src/loops/shipback.ts
116958
117320
  init_esm_shims();
@@ -117385,12 +117747,12 @@ function docMergeRefusal(boot, fresh, next, cleanAppend) {
117385
117747
  async function shipBackStateDocs(sdk, apiKey, slug, dir, boot) {
117386
117748
  const outcomes = [];
117387
117749
  for (const doc of boot) {
117388
- const path2 = join7(dir, doc.filename);
117389
- if (!existsSync9(path2)) {
117750
+ const path2 = join8(dir, doc.filename);
117751
+ if (!existsSync10(path2)) {
117390
117752
  outcomes.push({ key: doc.key, outcome: "unchanged" });
117391
117753
  continue;
117392
117754
  }
117393
- const materialized = readFileSync8(path2, "utf-8");
117755
+ const materialized = readFileSync9(path2, "utf-8");
117394
117756
  const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
117395
117757
  const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
117396
117758
  if (fresh === void 0) {
@@ -117460,20 +117822,20 @@ var TASK_DEFINITION_FILES = [
117460
117822
  ];
117461
117823
  function materializeTask(task, runId) {
117462
117824
  const dir = loopFireDir(task.slug, runId);
117463
- mkdirSync6(dir, { recursive: true, mode: 448 });
117825
+ mkdirSync7(dir, { recursive: true, mode: 448 });
117464
117826
  const files = {};
117465
117827
  const skipped = [];
117466
117828
  for (const [label2, filename, field] of TASK_DEFINITION_FILES) {
117467
117829
  const body = task[field];
117468
117830
  if (typeof body === "string" && body.length > 0) {
117469
- writeFileSync8(join8(dir, filename), body, { mode: 384 });
117831
+ writeFileSync8(join9(dir, filename), body, { mode: 384 });
117470
117832
  files[label2] = filename;
117471
117833
  } else {
117472
117834
  skipped.push(label2);
117473
117835
  }
117474
117836
  }
117475
117837
  writeFileSync8(
117476
- join8(dir, "STATUS.md"),
117838
+ join9(dir, "STATUS.md"),
117477
117839
  `# STATUS \u2014 ${task.slug}
117478
117840
 
117479
117841
  task_id: ${task.id}
@@ -117504,7 +117866,7 @@ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117504
117866
  notes.push(`memory doc '${f.key}' not materialized: ${f.reason}`);
117505
117867
  }
117506
117868
  for (const doc of fetched) {
117507
- writeFileSync8(join8(dir, doc.filename), doc.body, { mode: 384 });
117869
+ writeFileSync8(join9(dir, doc.filename), doc.body, { mode: 384 });
117508
117870
  files[`doc:${doc.key}`] = doc.filename;
117509
117871
  notes.push(describeSource(doc));
117510
117872
  }
@@ -117518,14 +117880,14 @@ async function materializeMemoryDocs(sdk, apiKey, task, dir) {
117518
117880
  }
117519
117881
 
117520
117882
  // src/mcp-server/index.ts
117521
- import { existsSync as existsSync12 } from "fs";
117883
+ import { existsSync as existsSync13 } from "fs";
117522
117884
 
117523
117885
  // src/loops/dashboard.ts
117524
117886
  init_esm_shims();
117525
117887
  import { createServer as createServer2 } from "http";
117526
117888
  import * as realFs from "fs";
117527
117889
  import { spawn as realSpawn } from "child_process";
117528
- import { join as join9 } from "path";
117890
+ import { join as join10 } from "path";
117529
117891
  var DEFAULT_PORT = 4477;
117530
117892
  var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
117531
117893
  var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
@@ -117586,14 +117948,14 @@ function startDashboardServer(args) {
117586
117948
  const data = {};
117587
117949
  for (const name of files) {
117588
117950
  try {
117589
- const p = join9(args.loopDir, name);
117951
+ const p = join10(args.loopDir, name);
117590
117952
  if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
117591
117953
  } catch {
117592
117954
  }
117593
117955
  }
117594
117956
  const state = {};
117595
117957
  try {
117596
- const p = join9(args.loopDir, ".state", "fires.jsonl");
117958
+ const p = join10(args.loopDir, ".state", "fires.jsonl");
117597
117959
  if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
117598
117960
  } catch {
117599
117961
  }
@@ -117992,7 +118354,7 @@ function injectDefaultDashboard(loop2) {
117992
118354
 
117993
118355
  // src/loops/memory-verbs.ts
117994
118356
  init_esm_shims();
117995
- import { readFileSync as readFileSync9 } from "fs";
118357
+ import { readFileSync as readFileSync10 } from "fs";
117996
118358
 
117997
118359
  // src/loops/sdk.ts
117998
118360
  init_esm_shims();
@@ -118252,7 +118614,7 @@ async function taskMemoryAppendVerb(rawSlug, opts) {
118252
118614
  const archived = opts.archived === true;
118253
118615
  const note = opts.note?.trim() || void 0;
118254
118616
  const scope = parseScopeFlag(opts.scope, false);
118255
- const content = opts.file !== void 0 ? readFileSync9(opts.file, "utf-8") : opts.content ?? "";
118617
+ const content = opts.file !== void 0 ? readFileSync10(opts.file, "utf-8") : opts.content ?? "";
118256
118618
  if (!content) {
118257
118619
  throw new Error("nothing to store: pass --content <text> or --file <path>");
118258
118620
  }
@@ -118389,14 +118751,14 @@ ${full.markdownBody ?? ""}`;
118389
118751
 
118390
118752
  // src/mcp-server/read-file-body.ts
118391
118753
  init_esm_shims();
118392
- import { readFileSync as readFileSync10 } from "fs";
118754
+ import { readFileSync as readFileSync11 } from "fs";
118393
118755
  import { resolve as resolvePath } from "path";
118394
118756
  function readMarkdownFile(filePath) {
118395
118757
  const raw = typeof filePath === "string" ? filePath.trim() : "";
118396
118758
  if (!raw) throw new Error("filePath is empty.");
118397
118759
  const abs = resolvePath(raw);
118398
118760
  try {
118399
- return readFileSync10(abs, "utf8");
118761
+ return readFileSync11(abs, "utf8");
118400
118762
  } catch (err) {
118401
118763
  const reason = err instanceof Error ? err.message : String(err);
118402
118764
  throw new Error(`Could not read file at "${abs}": ${reason}`);
@@ -118725,7 +119087,7 @@ function enforceResponseCap(payload) {
118725
119087
 
118726
119088
  // src/mcp-server/delegate-tools.ts
118727
119089
  init_esm_shims();
118728
- import { existsSync as existsSync10, statSync as statSync2 } from "fs";
119090
+ import { existsSync as existsSync11, statSync as statSync2 } from "fs";
118729
119091
  import { z as z3 } from "zod";
118730
119092
 
118731
119093
  // src/delegate/jobs.ts
@@ -118827,7 +119189,7 @@ import * as nodeFs from "fs";
118827
119189
  import { execFileSync } from "child_process";
118828
119190
  import { createHash } from "crypto";
118829
119191
  import { homedir as homedir5 } from "os";
118830
- import { join as join10 } from "path";
119192
+ import { join as join11 } from "path";
118831
119193
  var OPENCODE_VERSION = "1.18.13";
118832
119194
  var OPENCODE_CHECKSUMS = {
118833
119195
  "darwin-arm64": "6a85ae6de1aeb8e39ae4d977337b03f49168c2a827ee37b6f82c39471d711c63",
@@ -118849,10 +119211,10 @@ function resolveOpencodePlatform(platform, arch) {
118849
119211
  return void 0;
118850
119212
  }
118851
119213
  function opencodeBinDir(home = homedir5()) {
118852
- return join10(home, `.ametyst${ENV_SUFFIX}`, "bin");
119214
+ return join11(home, `.ametyst${ENV_SUFFIX}`, "bin");
118853
119215
  }
118854
119216
  function opencodeBinaryPath(home = homedir5()) {
118855
- return join10(opencodeBinDir(home), `opencode-${OPENCODE_VERSION}`);
119217
+ return join11(opencodeBinDir(home), `opencode-${OPENCODE_VERSION}`);
118856
119218
  }
118857
119219
  function opencodeSidecarPath(home = homedir5()) {
118858
119220
  return `${opencodeBinaryPath(home)}.sha256`;
@@ -118907,7 +119269,7 @@ async function ensureOpencodeBinary(deps = {}) {
118907
119269
  const bytes = new Uint8Array(await res.arrayBuffer());
118908
119270
  const expected = (deps.checksums ?? OPENCODE_CHECKSUMS)[key];
118909
119271
  const actual = sha256Hex(bytes);
118910
- const tmpArchive = join10(binDir, `.${artifact}.download-${process.pid}`);
119272
+ const tmpArchive = join11(binDir, `.${artifact}.download-${process.pid}`);
118911
119273
  fs.writeFileSync(tmpArchive, bytes);
118912
119274
  if (actual !== expected) {
118913
119275
  fs.rmSync(tmpArchive, { force: true });
@@ -118915,7 +119277,7 @@ async function ensureOpencodeBinary(deps = {}) {
118915
119277
  `opencode bootstrap: checksum mismatch for ${artifact} (expected ${expected}, got ${actual}). The download was deleted; refusing to install an unverified binary.`
118916
119278
  );
118917
119279
  }
118918
- const tmpExtractDir = join10(binDir, `.extract-${OPENCODE_VERSION}-${process.pid}`);
119280
+ const tmpExtractDir = join11(binDir, `.extract-${OPENCODE_VERSION}-${process.pid}`);
118919
119281
  fs.rmSync(tmpExtractDir, { recursive: true, force: true });
118920
119282
  fs.mkdirSync(tmpExtractDir, { recursive: true, mode: 448 });
118921
119283
  try {
@@ -118924,7 +119286,7 @@ async function ensureOpencodeBinary(deps = {}) {
118924
119286
  } else {
118925
119287
  exec("tar", ["-xzf", tmpArchive, "-C", tmpExtractDir]);
118926
119288
  }
118927
- const extracted = join10(tmpExtractDir, "opencode");
119289
+ const extracted = join11(tmpExtractDir, "opencode");
118928
119290
  if (!fs.existsSync(extracted)) {
118929
119291
  const entries = fs.readdirSync(tmpExtractDir).join(", ") || "<empty>";
118930
119292
  throw new Error(
@@ -118995,15 +119357,15 @@ import { randomUUID } from "crypto";
118995
119357
  init_esm_shims();
118996
119358
  init_paths();
118997
119359
  import { homedir as homedir6 } from "os";
118998
- import { join as join11 } from "path";
119360
+ import { join as join12 } from "path";
118999
119361
  function bridgeRunDir(home = homedir6()) {
119000
- return join11(home, `.ametyst${ENV_SUFFIX}`, "run");
119362
+ return join12(home, `.ametyst${ENV_SUFFIX}`, "run");
119001
119363
  }
119002
119364
  function bridgeSocketPath(pid, home = homedir6()) {
119003
- return join11(bridgeRunDir(home), `delegate-${pid}.sock`);
119365
+ return join12(bridgeRunDir(home), `delegate-${pid}.sock`);
119004
119366
  }
119005
119367
  function bridgeNoncePath(pid, home = homedir6()) {
119006
- return join11(bridgeRunDir(home), `delegate-${pid}.nonce`);
119368
+ return join12(bridgeRunDir(home), `delegate-${pid}.nonce`);
119007
119369
  }
119008
119370
  function bridgeNoncePathForSocket(socketPath) {
119009
119371
  return socketPath.replace(/\.sock$/, ".nonce");
@@ -119025,7 +119387,7 @@ function listBridgeSockets(fs, home = homedir6()) {
119025
119387
  const entries = [];
119026
119388
  for (const name of names) {
119027
119389
  if (pidFromSocketName(name) === void 0) continue;
119028
- const path2 = join11(dir, name);
119390
+ const path2 = join12(dir, name);
119029
119391
  try {
119030
119392
  entries.push({ path: path2, mtimeMs: fs.statSync(path2).mtimeMs });
119031
119393
  } catch {
@@ -119374,7 +119736,7 @@ async function startShim(deps) {
119374
119736
  init_esm_shims();
119375
119737
  import * as nodeFs3 from "fs";
119376
119738
  import { tmpdir } from "os";
119377
- import { join as join12 } from "path";
119739
+ import { join as join13 } from "path";
119378
119740
  var DELEGATE_PROVIDER_ID = "ametyst";
119379
119741
  var DELEGATE_SHIM_API_KEY = "ametyst-local-shim";
119380
119742
  function buildDelegateConfig(opts) {
@@ -119398,8 +119760,8 @@ function delegateModelRef(model) {
119398
119760
  }
119399
119761
  function writeTempDelegateConfig(config, deps = {}) {
119400
119762
  const fs = deps.fs ?? nodeFs3;
119401
- const dir = fs.mkdtempSync(join12(deps.tmp ?? tmpdir(), "ametyst-delegate-"));
119402
- const path2 = join12(dir, "opencode.json");
119763
+ const dir = fs.mkdtempSync(join13(deps.tmp ?? tmpdir(), "ametyst-delegate-"));
119764
+ const path2 = join13(dir, "opencode.json");
119403
119765
  fs.writeFileSync(path2, `${JSON.stringify(config, null, 2)}
119404
119766
  `, { mode: 384 });
119405
119767
  let done = false;
@@ -119468,7 +119830,7 @@ init_paths();
119468
119830
  import * as nodeFs4 from "fs";
119469
119831
  import { execFileSync as execFileSync2 } from "child_process";
119470
119832
  import { homedir as homedir8 } from "os";
119471
- import { basename as basename4, join as join13 } from "path";
119833
+ import { basename as basename4, join as join14 } from "path";
119472
119834
  var DELEGATED_CHILD_ENV_VAR = "AMETYST_DELEGATED";
119473
119835
  var SPEND_GRANT_TOKEN_ENV_VAR = "AMETYST_DELEGATE_SPEND_TOKEN";
119474
119836
  var SPEND_KILL_SWITCH_ENV_VAR = "AMETYST_DELEGATE_NO_SPEND";
@@ -119550,11 +119912,11 @@ function isDelegatedByAncestry(deps = {}) {
119550
119912
  return classifyAncestry(deps) === "delegated";
119551
119913
  }
119552
119914
  function delegateChildConfigHome(home = homedir8()) {
119553
- return join13(home, `.ametyst${ENV_SUFFIX}`, "delegate-xdg");
119915
+ return join14(home, `.ametyst${ENV_SUFFIX}`, "delegate-xdg");
119554
119916
  }
119555
119917
  function ensureChildConfigHome(fs = nodeFs4, home = homedir8()) {
119556
119918
  const root2 = delegateChildConfigHome(home);
119557
- const inner = join13(root2, "opencode");
119919
+ const inner = join14(root2, "opencode");
119558
119920
  if (!fs.existsSync(inner)) fs.mkdirSync(inner, { recursive: true, mode: 448 });
119559
119921
  return root2;
119560
119922
  }
@@ -119564,7 +119926,7 @@ function userOpencodeConfigDir(deps = {}) {
119564
119926
  const env = deps.env ?? process.env;
119565
119927
  const home = deps.home ?? homedir8();
119566
119928
  const xdg = env.XDG_CONFIG_HOME?.trim();
119567
- return join13(xdg ? xdg : join13(home, ".config"), "opencode");
119929
+ return join14(xdg ? xdg : join14(home, ".config"), "opencode");
119568
119930
  }
119569
119931
  function stripJsonComments(text) {
119570
119932
  let out = "";
@@ -119608,9 +119970,9 @@ function readUserMcpServers(deps = {}) {
119608
119970
  const warn = deps.warn ?? ((line) => console.error(line));
119609
119971
  const home = deps.home ?? homedir8();
119610
119972
  const dir = userOpencodeConfigDir(deps);
119611
- if (dir === join13(delegateChildConfigHome(home), "opencode")) return {};
119973
+ if (dir === join14(delegateChildConfigHome(home), "opencode")) return {};
119612
119974
  for (const filename of USER_OPENCODE_CONFIG_FILENAMES) {
119613
- const path2 = join13(dir, filename);
119975
+ const path2 = join14(dir, filename);
119614
119976
  let raw;
119615
119977
  try {
119616
119978
  if (!fs.existsSync(path2)) continue;
@@ -120708,7 +121070,7 @@ init_esm_shims();
120708
121070
  import * as nodeFs6 from "fs";
120709
121071
  import { createHash as createHash2, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "crypto";
120710
121072
  import { homedir as homedir10 } from "os";
120711
- import { join as join14 } from "path";
121073
+ import { join as join15 } from "path";
120712
121074
  var SPEND_GRANT_RECORD_VERSION = 1;
120713
121075
  var TOKEN_BYTES = 32;
120714
121076
  var SPEND_GRANT_GRACE_MS = 5 * 6e4;
@@ -120716,7 +121078,7 @@ function spendGrantPath(jobId, home = homedir10()) {
120716
121078
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(jobId)) {
120717
121079
  throw new Error(`refusing to build a grant path for job id ${JSON.stringify(jobId)}`);
120718
121080
  }
120719
- return join14(bridgeRunDir(home), `spend-grant-${jobId}.json`);
121081
+ return join15(bridgeRunDir(home), `spend-grant-${jobId}.json`);
120720
121082
  }
120721
121083
  function hashSpendGrantToken(token) {
120722
121084
  return createHash2("sha256").update(token, "utf-8").digest("hex");
@@ -120822,13 +121184,13 @@ init_esm_shims();
120822
121184
  init_paths();
120823
121185
  import * as nodeFs7 from "fs";
120824
121186
  import { homedir as homedir11 } from "os";
120825
- import { dirname as dirname6, join as join15 } from "path";
121187
+ import { dirname as dirname6, join as join16 } from "path";
120826
121188
  var DELEGATE_CREDIT_CAPABILITY = "credit";
120827
121189
  var DELEGATE_CREDIT_PRICE_USD = 1;
120828
121190
  var DELEGATE_CREDIT_REQUEST_BODY = "{}";
120829
121191
  var DELEGATE_CREDIT_SPEND_SNIPPET = `spend({ merchant_slug: "${DELEGATE_MERCHANT_SLUG}", capability: "${DELEGATE_CREDIT_CAPABILITY}", inputs: "${DELEGATE_CREDIT_REQUEST_BODY}" })`;
120830
121192
  function creditLedgerPath(home = homedir11()) {
120831
- return join15(home, `.ametyst${ENV_SUFFIX}`, "run", "delegate-credit.json");
121193
+ return join16(home, `.ametyst${ENV_SUFFIX}`, "run", "delegate-credit.json");
120832
121194
  }
120833
121195
  function readCreditLedger(fs = nodeFs7, path2 = creditLedgerPath()) {
120834
121196
  try {
@@ -121219,7 +121581,7 @@ function registerDelegateTools(deps) {
121219
121581
  const validated = validateStartInput(params, {
121220
121582
  isDirectory: (path2) => {
121221
121583
  try {
121222
- return existsSync10(path2) && statSync2(path2).isDirectory();
121584
+ return existsSync11(path2) && statSync2(path2).isDirectory();
121223
121585
  } catch {
121224
121586
  return false;
121225
121587
  }
@@ -121474,7 +121836,7 @@ init_resolve();
121474
121836
 
121475
121837
  // src/mcp-server/identity-reload.ts
121476
121838
  init_esm_shims();
121477
- import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
121839
+ import { readFileSync as readFileSync13, statSync as statSync3 } from "fs";
121478
121840
  var NO_VAULT = "absent";
121479
121841
  var IDENTITY_CHECK_THROTTLE_MS = 1e3;
121480
121842
  function fingerprintVault(stat) {
@@ -121495,7 +121857,11 @@ var IdentityWatch = class {
121495
121857
  stat;
121496
121858
  /** False for an env-supplied key: `poll` is then a guaranteed no-op. */
121497
121859
  armed;
121860
+ configPath;
121861
+ readWorkspaceKey;
121498
121862
  fingerprint;
121863
+ configStamp;
121864
+ workspaceKey;
121499
121865
  lastCheckedAt = null;
121500
121866
  constructor(options) {
121501
121867
  this.vaultPath = options.vaultPath;
@@ -121503,12 +121869,43 @@ var IdentityWatch = class {
121503
121869
  this.throttleMs = options.throttleMs ?? IDENTITY_CHECK_THROTTLE_MS;
121504
121870
  this.stat = options.stat ?? statVaultFile;
121505
121871
  this.armed = options.source !== "env";
121506
- this.fingerprint = fingerprintVault(this.stat(this.vaultPath));
121872
+ this.configPath = options.configPath ?? null;
121873
+ this.readWorkspaceKey = options.readWorkspaceKey ?? null;
121874
+ this.configStamp = this.configPath ? fingerprintVault(this.stat(this.configPath)) : NO_VAULT;
121875
+ this.workspaceKey = this.readWorkspaceKeySafely();
121876
+ this.fingerprint = this.composeFingerprint();
121877
+ }
121878
+ /** Never let a config read take the watch — or the tool call behind it — down. */
121879
+ readWorkspaceKeySafely() {
121880
+ if (!this.readWorkspaceKey) return null;
121881
+ try {
121882
+ return this.readWorkspaceKey();
121883
+ } catch {
121884
+ return null;
121885
+ }
121886
+ }
121887
+ /**
121888
+ * Vault fingerprint AND workspace key in one string, so `poll` stays a single
121889
+ * comparison and neither half can change without being noticed.
121890
+ *
121891
+ * A watch with NO workspace reader appends nothing, so its fingerprint stays
121892
+ * exactly the string it was before 0.3.6 (`"<mtime>:<size>"` / `"absent"`).
121893
+ * That is not cosmetic: the fingerprint is an observable of this class, and a
121894
+ * vault-only watch must be indistinguishable from the one that shipped.
121895
+ */
121896
+ composeFingerprint() {
121897
+ const vault = fingerprintVault(this.stat(this.vaultPath));
121898
+ if (!this.readWorkspaceKey) return vault;
121899
+ return `${vault}|ws:${this.workspaceKey ?? "unbound"}`;
121507
121900
  }
121508
121901
  /** The fingerprint this watch currently considers current. */
121509
121902
  currentFingerprint() {
121510
121903
  return this.fingerprint;
121511
121904
  }
121905
+ /** The workspace key this watch last read from the config. */
121906
+ currentWorkspaceKey() {
121907
+ return this.workspaceKey;
121908
+ }
121512
121909
  /**
121513
121910
  * One throttled `stat`. Returns the change when the vault file differs from
121514
121911
  * the fingerprint on record — and ADOPTS it in the same step, so a reload the
@@ -121521,7 +121918,14 @@ var IdentityWatch = class {
121521
121918
  const at = this.now();
121522
121919
  if (this.lastCheckedAt !== null && at - this.lastCheckedAt < this.throttleMs) return null;
121523
121920
  this.lastCheckedAt = at;
121524
- const next = fingerprintVault(this.stat(this.vaultPath));
121921
+ if (this.configPath) {
121922
+ const stamp = fingerprintVault(this.stat(this.configPath));
121923
+ if (stamp !== this.configStamp) {
121924
+ this.configStamp = stamp;
121925
+ this.workspaceKey = this.readWorkspaceKeySafely();
121926
+ }
121927
+ }
121928
+ const next = this.composeFingerprint();
121525
121929
  if (next === this.fingerprint) return null;
121526
121930
  const from14 = this.fingerprint;
121527
121931
  this.fingerprint = next;
@@ -121537,7 +121941,7 @@ var IdentityWatch = class {
121537
121941
  */
121538
121942
  readVault() {
121539
121943
  try {
121540
- return readFileSync12(this.vaultPath, "utf-8");
121944
+ return readFileSync13(this.vaultPath, "utf-8");
121541
121945
  } catch {
121542
121946
  return null;
121543
121947
  }
@@ -121545,6 +121949,7 @@ var IdentityWatch = class {
121545
121949
  };
121546
121950
 
121547
121951
  // src/mcp-server/index.ts
121952
+ init_grant_keystore();
121548
121953
  function buildDeclaredConnectorsSentence(catalog, unreadable) {
121549
121954
  const read = catalog === void 0 ? { connectors: cachedCatalogOrEmpty(), unreadable: mirrorGap() } : { connectors: catalog, unreadable: unreadable ?? [] };
121550
121955
  const gap = describeCatalogGap(read.unreadable);
@@ -121555,6 +121960,82 @@ function buildDeclaredConnectorsSentence(catalog, unreadable) {
121555
121960
  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}`);
121556
121961
  }
121557
121962
  var currentCredentials = {};
121963
+ function vaultIsEncrypted() {
121964
+ return !plaintextVaultMode;
121965
+ }
121966
+ function ownedSigningAddresses() {
121967
+ const addresses = [];
121968
+ if (currentCredentials.eoaAddress) addresses.push(currentCredentials.eoaAddress);
121969
+ try {
121970
+ addresses.push(...listGrantAddresses({ workspaceId: readBoundWorkspace() }));
121971
+ } catch (err) {
121972
+ console.error(
121973
+ `\u26A0\uFE0F could not read the per-grant keystore \u2014 only the install EOA is selectable: ${err instanceof Error ? err.message : String(err)}`
121974
+ );
121975
+ }
121976
+ return addresses;
121977
+ }
121978
+ function isOwnedSigningAddress(address) {
121979
+ if (typeof address !== "string" || address.trim() === "") return false;
121980
+ const needle = address.toLowerCase();
121981
+ return ownedSigningAddresses().some((a) => a.toLowerCase() === needle);
121982
+ }
121983
+ function signingKeyForAddress(address, passphrase) {
121984
+ return resolveGrantSigningKey({
121985
+ address,
121986
+ vaultJson: currentCredentials.walletKeystoreJson,
121987
+ vaultAddress: currentCredentials.eoaAddress,
121988
+ // ⛔ THE CALLER'S PASSPHRASE WINS. `start_session` resolves the key DURING
121989
+ // `runStartSession`, before `mergeStartSessionCredentials` has put the
121990
+ // passphrase into `currentCredentials` — so reading only the field would
121991
+ // make every encrypted-vault unlock fail with "a passphrase is required"
121992
+ // on the very call that was handed one.
121993
+ passphrase: passphrase ?? currentCredentials.passphrase
121994
+ });
121995
+ }
121996
+ async function refreshSignerBinding() {
121997
+ if (!currentCredentials.apiKey) return { changed: false, address: currentCredentials.signerAddress };
121998
+ let wallets;
121999
+ try {
122000
+ wallets = await fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true);
122001
+ } catch (err) {
122002
+ console.error(
122003
+ `\u26A0\uFE0F [signer-binding] could not re-read the grants \u2014 signing with the cached binding (${currentCredentials.signerAddress ?? "none"}): ${err instanceof Error ? err.message : String(err)}`
122004
+ );
122005
+ wallets = walletsCache || [];
122006
+ }
122007
+ const row = currentCredentials.pendingWalletId ? findCurrentApprovedWallet(wallets, ownedSigningAddresses(), currentCredentials.pendingWalletId) : selectNewestApprovedWalletAmong(
122008
+ wallets,
122009
+ (Array.isArray(wallets) ? wallets : []).map((w) => w?.address)
122010
+ );
122011
+ if (!row) return { changed: false, address: currentCredentials.signerAddress };
122012
+ const previousSigner = currentCredentials.signerAddress;
122013
+ const { changed, signerAddress } = bindSignerToGrantRow(currentCredentials, row);
122014
+ if (changed) {
122015
+ console.error(
122016
+ `\u{1F501} [signer-binding] the active grant moved \u2014 signer ${previousSigner ?? "none"} -> ${signerAddress ?? "none"} (virtual wallet ${currentCredentials.virtualWalletId}, policy ${currentCredentials.policyId ?? "unknown"}). Dropping the signer state derived from the old grant.`
122017
+ );
122018
+ }
122019
+ return { changed, address: signerAddress };
122020
+ }
122021
+ async function resolveSignerForMaterialization(passphrase) {
122022
+ await refreshSignerBinding();
122023
+ const address = currentCredentials.signerAddress || currentCredentials.eoaAddress || "";
122024
+ if (!address) {
122025
+ throw new Error(
122026
+ "no signer address for this session \u2014 no approved grant and no install wallet. Run requestAccess first."
122027
+ );
122028
+ }
122029
+ return { address, privateKey: signingKeyForAddress(address, passphrase) };
122030
+ }
122031
+ function hasSigningKeyForAddress(address) {
122032
+ if (!address) return false;
122033
+ try {
122034
+ return hasGrantSigningKey({ address, vaultAddress: currentCredentials.eoaAddress });
122035
+ } catch {
122036
+ return false;
122037
+ }
122038
+ }
121558
122039
  var plaintextVaultMode = false;
121559
122040
  function refreshPlaintextVaultMode() {
121560
122041
  const vault = currentCredentials.walletKeystoreJson;
@@ -122576,7 +123057,16 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
122576
123057
  currentCredentials.signerAddress = eoaAddress;
122577
123058
  currentCredentials.eoaAddress = eoaAddress;
122578
123059
  currentCredentials.apiKey = config.apiKey;
122579
- identityWatch = new IdentityWatch({ vaultPath: VAULT_PATH, source: config.apiKeySource ?? null });
123060
+ identityWatch = new IdentityWatch({
123061
+ vaultPath: VAULT_PATH,
123062
+ source: config.apiKeySource ?? null,
123063
+ // The workspace half (0.3.6): a `login`/`init --claim` that re-binds this
123064
+ // install to a DIFFERENT workspace changes the recorded key, and the same
123065
+ // reload that follows a swapped `wallet.json` drops the state cached for
123066
+ // the old one. See ./identity-reload.ts.
123067
+ configPath: CONFIG_PATH,
123068
+ readWorkspaceKey: readBoundWorkspace
123069
+ });
122580
123070
  mcpEventLogger = new McpEventLogger(
122581
123071
  () => getSDK(),
122582
123072
  () => currentCredentials.apiKey,
@@ -122610,12 +123100,15 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
122610
123100
  const wallets = walletsCache || [];
122611
123101
  const approvedWallet = findCurrentApprovedWallet(
122612
123102
  wallets,
122613
- eoaAddress,
123103
+ ownedSigningAddresses(),
122614
123104
  currentCredentials.pendingWalletId
122615
123105
  );
122616
123106
  if (approvedWallet) {
122617
123107
  console.error("\u2705 Found approved wallet from backend \u2014 authorization data stored");
122618
123108
  currentCredentials.authorizationStatus = "approved";
123109
+ if (typeof approvedWallet.address === "string" && approvedWallet.address.trim() !== "") {
123110
+ currentCredentials.signerAddress = approvedWallet.address;
123111
+ }
122619
123112
  currentCredentials.virtualWalletStatus = approvedWallet.status;
122620
123113
  currentCredentials.walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
122621
123114
  currentCredentials.virtualWalletId = String(approvedWallet.id);
@@ -122662,12 +123155,9 @@ async function createKernelClientFromVault() {
122662
123155
  throw new Error("No passphrase or keystore available");
122663
123156
  }
122664
123157
  if (!cliConfig) throw new Error("CLI config not initialized");
122665
- const { readVaultPrivateKey: readVaultPrivateKey2 } = await Promise.resolve().then(() => (init_core(), core_exports));
122666
- let privateKey = readVaultPrivateKey2(
122667
- currentCredentials.walletKeystoreJson,
122668
- currentCredentials.passphrase
122669
- );
122670
- console.error(`${ts()} Wallet key loaded`);
123158
+ const signer = await resolveSignerForMaterialization();
123159
+ let privateKey = signer.privateKey;
123160
+ console.error(`${ts()} Wallet key loaded \u2014 signing as ${signer.address}`);
122671
123161
  try {
122672
123162
  const { virtualWalletsManagers: virtualWalletsManagers2, financialAccounts: financialAccounts2 } = await getSDK();
122673
123163
  console.error(`${ts()} Fetching wallet data by API key...`);
@@ -122806,12 +123296,17 @@ async function tryResolvePendingApproval(probe) {
122806
123296
  if (probe) probe.fetched = true;
122807
123297
  const approvedWallet = findCurrentApprovedWallet(
122808
123298
  freshWallets,
122809
- currentCredentials.eoaAddress,
123299
+ // Post-rotation the row belongs to the ephemeral key this request minted,
123300
+ // not to the install EOA — match on every address we hold a key for.
123301
+ ownedSigningAddresses(),
122810
123302
  currentCredentials.pendingWalletId
122811
123303
  );
122812
123304
  if (!approvedWallet) return false;
122813
123305
  console.error("\u2705 [approval-wait] Backend says approved \u2014 syncing");
122814
123306
  currentCredentials.authorizationStatus = "approved";
123307
+ if (typeof approvedWallet.address === "string" && approvedWallet.address.trim() !== "") {
123308
+ currentCredentials.signerAddress = approvedWallet.address;
123309
+ }
122815
123310
  currentCredentials.walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
122816
123311
  currentCredentials.virtualWalletId = String(approvedWallet.id);
122817
123312
  currentCredentials.paymentManagerAddress = approvedWallet.paymentManagerAddress;
@@ -122938,6 +123433,12 @@ async function applyStartSessionUnlock(passphrase, resetRequested) {
122938
123433
  // this process loaded at boot verifies nothing: a grant approved since boot
122939
123434
  // would be invisible to exactly the check that exists to find it.
122940
123435
  fetchWallets: () => fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true),
123436
+ // ROTATION (0.3.6). Grant selection must span every address this install
123437
+ // holds a key for, and the key that signs must be the SELECTED row's —
123438
+ // not whatever `wallet.json` happens to contain. Both are injected so
123439
+ // `start-session.ts` keeps no dependency on the keystore's fs layer.
123440
+ candidateAddresses: ownedSigningAddresses(),
123441
+ resolveSigningKey: (address) => signingKeyForAddress(address, passphrase),
122941
123442
  readPolicyActiveOnchain: buildPolicyActiveOnchainReader(sdk)
122942
123443
  }
122943
123444
  );
@@ -123351,11 +123852,15 @@ server.tool(
123351
123852
  };
123352
123853
  }
123353
123854
  const { virtualWalletsManagers: virtualWalletsManagers2 } = await getSDK();
123354
- const result = await submitAccessRequest(
123855
+ const result = await submitRotatedAccessRequest(
123355
123856
  virtualWalletsManagers2,
123356
123857
  currentCredentials.apiKey,
123357
- currentCredentials.eoaAddress,
123358
- policyId
123858
+ policyId,
123859
+ createGrantKeyIssuer({
123860
+ encrypted: vaultIsEncrypted(),
123861
+ passphrase: currentCredentials.passphrase,
123862
+ workspaceId: readBoundWorkspace()
123863
+ })
123359
123864
  );
123360
123865
  currentCredentials.authorizationStatus = "pending";
123361
123866
  currentCredentials.pendingWalletId = result.id ? String(result.id) : void 0;
@@ -123413,7 +123918,7 @@ server.tool(
123413
123918
  server.tool(
123414
123919
  {
123415
123920
  name: "getWalletStatus",
123416
- 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.",
123921
+ description: "Get wallet status: auth, balance, policy, allowlist, WHICH GRANT AND KEY THIS SESSION SIGNS WITH, WHICH WORKSPACE this server is acting in, and the last 10 transactions (amount, merchant, timestamp, status). `signerAddress` is the address of the approved grant this session signs with \u2014 since every access request now mints its OWN ephemeral session key, it is NOT the same as `eoaAddress` (the install wallet) except for a grant approved before rotation; `activeVirtualWalletId` / `activePolicyId` name that grant's row, and `signerKeyPresent` says whether this machine still holds the private key for `signerAddress` \u2014 `false` means no spend can be signed until `requestAccess` mints a fresh session key and an admin approves it. `workspace` is `{companyName, employeeName}` \u2014 the Ametyst workspace every task, memory document and payment from this server lands in; both fields are null when the profile could not be read, which means UNKNOWN, never 'no workspace'. A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
123417
123922
  inputs: []
123418
123923
  },
123419
123924
  async () => {
@@ -123445,9 +123950,12 @@ server.tool(
123445
123950
  fetchVirtualWalletsFromBackend(currentCredentials.apiKey, !refreshedFromBackend),
123446
123951
  fetchPoliciesFromBackend(currentCredentials.apiKey, true)
123447
123952
  ]);
123448
- const freshWallet = selectNewestApprovedWallet(freshWallets, currentCredentials.eoaAddress);
123953
+ const freshWallet = selectNewestApprovedWalletAmong(freshWallets, ownedSigningAddresses());
123449
123954
  const freshWalletIsActiveGrant = String(freshWallet?.id) === String(currentCredentials.virtualWalletId);
123450
123955
  if (freshWallet && freshWalletIsActiveGrant) {
123956
+ if (typeof freshWallet.address === "string" && freshWallet.address.trim() !== "") {
123957
+ currentCredentials.signerAddress = freshWallet.address;
123958
+ }
123451
123959
  if (freshWallet.policyAssociated != null) {
123452
123960
  currentCredentials.policyId = String(freshWallet.policyAssociated);
123453
123961
  }
@@ -123466,6 +123974,24 @@ server.tool(
123466
123974
  authorized: currentCredentials.authorizationStatus === "approved",
123467
123975
  status: currentCredentials.authorizationStatus || "none",
123468
123976
  eoaAddress: currentCredentials.eoaAddress || null,
123977
+ // WHICH KEY IS SIGNING. Since 0.3.6 every grant gets its OWN ephemeral
123978
+ // session key, so `signerAddress` is no longer a synonym for
123979
+ // `eoaAddress`: it is the address of the approved row this session
123980
+ // adopted, and `eoaAddress` is the install identity in `wallet.json`.
123981
+ // They agree only for a pre-rotation grant. Reporting both is what makes
123982
+ // "which key signed this" readable from a payload instead of from a
123983
+ // nonce-key autopsy — the same lesson as `activeVirtualWalletId` below.
123984
+ signerAddress: currentCredentials.signerAddress || currentCredentials.eoaAddress || null,
123985
+ // CAN THIS MACHINE ACTUALLY SIGN FOR THAT GRANT? A rotated grant is
123986
+ // signable only from the per-grant keystore entry minted with it, and a
123987
+ // machine that lost it (a re-install, a restored home, a grant approved
123988
+ // on another laptop) is bound to a signer it cannot produce. Presence
123989
+ // only — no decryption, so an encrypted vault with no cached passphrase
123990
+ // still reports `true` here and bounces at spend time on the passphrase,
123991
+ // which is a different and recoverable problem.
123992
+ signerKeyPresent: hasSigningKeyForAddress(
123993
+ currentCredentials.signerAddress || currentCredentials.eoaAddress
123994
+ ),
123469
123995
  walletAddress: currentCredentials.walletAddress || null,
123470
123996
  kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient,
123471
123997
  // WHICH GRANT THIS SESSION SIGNS WITH. The 2026-09-01 incident — three
@@ -123489,9 +124015,7 @@ server.tool(
123489
124015
  }
123490
124016
  }
123491
124017
  if (walletsCache && currentCredentials.eoaAddress) {
123492
- const wallet = selectNewestApprovedWallet(walletsCache, currentCredentials.eoaAddress) ?? walletsCache.find(
123493
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
123494
- );
124018
+ const wallet = selectNewestApprovedWalletAmong(walletsCache, ownedSigningAddresses()) ?? walletsCache.find((w) => isOwnedSigningAddress(w.address));
123495
124019
  if (wallet) {
123496
124020
  const now = Math.floor(Date.now() / 1e3);
123497
124021
  const validUntil = wallet.policyValidUntil;
@@ -124015,7 +124539,7 @@ async function startLiveDashboard(entity, dir) {
124015
124539
  clearInterval(watcher);
124016
124540
  return;
124017
124541
  }
124018
- if (!existsSync12(dir)) {
124542
+ if (!existsSync13(dir)) {
124019
124543
  clearInterval(watcher);
124020
124544
  liveDashboards.delete(slug);
124021
124545
  try {
@@ -124944,11 +125468,13 @@ server.tool(
124944
125468
  };
124945
125469
  }
124946
125470
  touchWalletActivity();
125471
+ let missingSignerKey;
124947
125472
  if (!currentCredentials.virtualWalletKernelAccountClient && currentCredentials.authorizationStatus === "approved" && isWalletUnlocked()) {
124948
125473
  try {
124949
125474
  console.error("\u{1F504} [spend] Auto-creating kernel client...");
124950
125475
  await createKernelClientFromVault();
124951
125476
  } catch (kcErr) {
125477
+ if (isMissingGrantKeyError(kcErr)) missingSignerKey = kcErr;
124952
125478
  console.error("\u274C [spend] Auto kernel client creation failed:", kcErr instanceof Error ? kcErr.message : String(kcErr));
124953
125479
  }
124954
125480
  }
@@ -124962,10 +125488,13 @@ server.tool(
124962
125488
  }
124963
125489
  const approvedWallet = findCurrentApprovedWallet(
124964
125490
  wallets,
124965
- currentCredentials.eoaAddress,
125491
+ ownedSigningAddresses(),
124966
125492
  currentCredentials.pendingWalletId
124967
125493
  );
124968
125494
  if (approvedWallet && currentCredentials.authorizationStatus !== "approved") {
125495
+ if (typeof approvedWallet.address === "string" && approvedWallet.address.trim() !== "") {
125496
+ currentCredentials.signerAddress = approvedWallet.address;
125497
+ }
124969
125498
  currentCredentials.walletAddress = approvedWallet.walletAddress || approvedWallet.kernelAccountAddress;
124970
125499
  currentCredentials.virtualWalletId = String(approvedWallet.id);
124971
125500
  currentCredentials.paymentManagerAddress = approvedWallet.paymentManagerAddress;
@@ -125001,6 +125530,28 @@ server.tool(
125001
125530
  const apiKey = currentCredentials.apiKey;
125002
125531
  if (!kernelClient || !apiKey) {
125003
125532
  console.error("\u26A0\uFE0F [spend] Missing kernel client or API key in state");
125533
+ if (missingSignerKey) {
125534
+ logSpendTelemetryEarlyErr();
125535
+ return {
125536
+ content: [
125537
+ {
125538
+ type: "text",
125539
+ text: JSON.stringify({
125540
+ success: false,
125541
+ ...disclosePayment(),
125542
+ // PRE-PAYMENT
125543
+ error: "signer_key_missing",
125544
+ detail: missingSignerKey.message,
125545
+ guidance: {
125546
+ say_to_user: "This machine can't sign for your approved access any more \u2014 the session key it was issued to isn't here. I need to request access again so a fresh key can be approved.",
125547
+ next_action: "Call getAvailablePolicies(), let the user pick a policy, then call requestAccess with it. Once the admin approves the new request, retry this spend.",
125548
+ stop: true
125549
+ }
125550
+ })
125551
+ }
125552
+ ]
125553
+ };
125554
+ }
125004
125555
  if (currentCredentials.authorizationStatus === "pending") {
125005
125556
  logSpendTelemetryEarlyErr();
125006
125557
  return {
@@ -125854,7 +126405,7 @@ async function reconcileApprovalStateOnConnect() {
125854
126405
  }
125855
126406
  try {
125856
126407
  const wallets = await fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true);
125857
- const approved = selectNewestApprovedWallet(wallets, currentCredentials.eoaAddress);
126408
+ const approved = selectNewestApprovedWalletAmong(wallets, ownedSigningAddresses());
125858
126409
  if (!approved) return;
125859
126410
  if (typeof approved.id === "number" && hasProcessedApprovalEventId(approved.id)) return;
125860
126411
  console.error(
@@ -125869,9 +126420,7 @@ async function reconcileApprovalStateOnConnect() {
125869
126420
  policyOnchainPermissions: void 0
125870
126421
  });
125871
126422
  if (walletsCache) {
125872
- const idx = walletsCache.findIndex(
125873
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
125874
- );
126423
+ const idx = walletsCache.findIndex((w) => isOwnedSigningAddress(w.address));
125875
126424
  if (idx >= 0) walletsCache[idx] = { ...walletsCache[idx], status: "approved" };
125876
126425
  }
125877
126426
  if (typeof approved.id === "number") rememberProcessedApprovalEventId(approved.id);
@@ -125904,8 +126453,8 @@ async function setupWebSocketListener() {
125904
126453
  return;
125905
126454
  }
125906
126455
  const statusMatch = event.status === "approved" || event.status === "ok";
125907
- const addressMatch = event.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase();
125908
- console.error(`\u{1F50D} [WebSocket] statusMatch=${statusMatch} addressMatch=${addressMatch} eoaAddress=${currentCredentials.eoaAddress}`);
126456
+ const addressMatch = isOwnedSigningAddress(event.address);
126457
+ console.error(`\u{1F50D} [WebSocket] statusMatch=${statusMatch} addressMatch=${addressMatch} eoaAddress=${currentCredentials.eoaAddress} signerAddress=${currentCredentials.signerAddress}`);
125909
126458
  if (statusMatch && addressMatch) {
125910
126459
  try {
125911
126460
  await onAuthorizationApproved({
@@ -125917,9 +126466,7 @@ async function setupWebSocketListener() {
125917
126466
  policyOnchainPermissions: void 0
125918
126467
  });
125919
126468
  if (walletsCache) {
125920
- const idx = walletsCache.findIndex(
125921
- (w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
125922
- );
126469
+ const idx = walletsCache.findIndex((w) => isOwnedSigningAddress(w.address));
125923
126470
  if (idx >= 0) {
125924
126471
  walletsCache[idx] = { ...walletsCache[idx], status: "approved" };
125925
126472
  }
@@ -126192,15 +126739,15 @@ function killPreviousServeInstances(deps) {
126192
126739
 
126193
126740
  // src/commands/autosync-skills.ts
126194
126741
  init_esm_shims();
126195
- import { existsSync as existsSync14 } from "fs";
126742
+ import { existsSync as existsSync15 } from "fs";
126196
126743
  import { homedir as homedir12 } from "os";
126197
- import { join as join18 } from "path";
126744
+ import { join as join19 } from "path";
126198
126745
 
126199
126746
  // src/compounds/sync-skills.ts
126200
126747
  init_esm_shims();
126201
126748
  init_paths();
126202
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
126203
- import { join as join17 } from "path";
126749
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync10 } from "fs";
126750
+ import { join as join18 } from "path";
126204
126751
  var MANAGED_MARKER = "<!-- ametyst-managed: sync-skills -->";
126205
126752
  var GITIGNORE_HEADER = "# ametyst-managed: sync-skills \u2014 pointer skills generated for this account; never commit them.";
126206
126753
  var GITIGNORE_HEADER_2 = "# Maintained by `ametyst serve` on every boot. Real skills without the managed marker are not listed.";
@@ -126244,22 +126791,22 @@ ${taskRunSection(slug, kind)}`;
126244
126791
  }
126245
126792
  function isManaged(file) {
126246
126793
  try {
126247
- return readFileSync13(file, "utf8").includes(MANAGED_MARKER);
126794
+ return readFileSync14(file, "utf8").includes(MANAGED_MARKER);
126248
126795
  } catch {
126249
126796
  return false;
126250
126797
  }
126251
126798
  }
126252
126799
  function listManagedDirs(root2) {
126253
- return readdirSync4(root2, { withFileTypes: true }).filter((e) => e.isDirectory() && isManaged(join17(root2, e.name, "SKILL.md"))).map((e) => e.name).sort();
126800
+ return readdirSync5(root2, { withFileTypes: true }).filter((e) => e.isDirectory() && isManaged(join18(root2, e.name, "SKILL.md"))).map((e) => e.name).sort();
126254
126801
  }
126255
126802
  function buildGitignoreContent(managedSlugs) {
126256
126803
  return [GITIGNORE_HEADER, GITIGNORE_HEADER_2, ".gitignore", ...managedSlugs.map((s) => `/${s}/`)].join("\n") + "\n";
126257
126804
  }
126258
126805
  function maintainGitignore(root2) {
126259
- const file = join17(root2, ".gitignore");
126806
+ const file = join18(root2, ".gitignore");
126260
126807
  const managed = listManagedDirs(root2);
126261
- if (existsSync13(file)) {
126262
- const current = readFileSync13(file, "utf8");
126808
+ if (existsSync14(file)) {
126809
+ const current = readFileSync14(file, "utf8");
126263
126810
  const firstLine = current.split(/\r?\n/, 1)[0];
126264
126811
  if (firstLine !== GITIGNORE_HEADER) {
126265
126812
  console.error(
@@ -126317,26 +126864,26 @@ async function syncSkills(opts = {}) {
126317
126864
  if (!desired.has(dir)) desired.set(dir, item);
126318
126865
  }
126319
126866
  const root2 = skillsRoot(target, global2);
126320
- mkdirSync8(root2, { recursive: true });
126867
+ mkdirSync9(root2, { recursive: true });
126321
126868
  let written = 0;
126322
126869
  const skipped = [];
126323
126870
  for (const [dir, item] of desired) {
126324
- const file = join17(dir, "SKILL.md");
126325
- if (existsSync13(file) && !isManaged(file)) {
126871
+ const file = join18(dir, "SKILL.md");
126872
+ if (existsSync14(file) && !isManaged(file)) {
126326
126873
  skipped.push(item.slug);
126327
126874
  console.warn(`\u26A0\uFE0F sync-skills: skipping "${item.slug}" \u2014 an unmanaged skill already exists at ${file}`);
126328
126875
  continue;
126329
126876
  }
126330
- mkdirSync8(dir, { recursive: true });
126877
+ mkdirSync9(dir, { recursive: true });
126331
126878
  writeFileSync10(file, buildStubContent(item));
126332
126879
  written++;
126333
126880
  }
126334
126881
  let pruned = 0;
126335
- for (const entry of readdirSync4(root2, { withFileTypes: true })) {
126882
+ for (const entry of readdirSync5(root2, { withFileTypes: true })) {
126336
126883
  if (!entry.isDirectory()) continue;
126337
- const dir = join17(root2, entry.name);
126884
+ const dir = join18(root2, entry.name);
126338
126885
  if (desired.has(dir)) continue;
126339
- if (!isManaged(join17(dir, "SKILL.md"))) continue;
126886
+ if (!isManaged(join18(dir, "SKILL.md"))) continue;
126340
126887
  rmSync(dir, { recursive: true, force: true });
126341
126888
  pruned++;
126342
126889
  }
@@ -126350,12 +126897,12 @@ function isSkillsAutosyncEnabled(env = process.env) {
126350
126897
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
126351
126898
  }
126352
126899
  function detectPresentTargets(deps = {}) {
126353
- const exists = deps.existsSync ?? existsSync14;
126900
+ const exists = deps.existsSync ?? existsSync15;
126354
126901
  const cwd = deps.cwd ?? (() => process.cwd());
126355
126902
  const home = deps.homedir ?? homedir12;
126356
126903
  const targets = [];
126357
- if (exists(join18(cwd(), ".claude")) || exists(join18(home(), ".claude"))) targets.push("claude");
126358
- if (exists(join18(cwd(), ".codex")) || exists(join18(home(), ".codex", "config.toml"))) targets.push("codex");
126904
+ if (exists(join19(cwd(), ".claude")) || exists(join19(home(), ".claude"))) targets.push("claude");
126905
+ if (exists(join19(cwd(), ".codex")) || exists(join19(home(), ".codex", "config.toml"))) targets.push("codex");
126359
126906
  return targets;
126360
126907
  }
126361
126908
  function resolveAutosyncTargets(clientName, deps = {}) {
@@ -127045,14 +127592,14 @@ init_esm_shims();
127045
127592
  // src/loops/materialize.ts
127046
127593
  init_esm_shims();
127047
127594
  init_paths();
127048
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
127049
- import { join as join19 } from "path";
127595
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
127596
+ import { join as join20 } from "path";
127050
127597
  var MIRRORED_DEFINITION_FILES = ["SKILL.md", "VISION.md", "README.md"];
127051
127598
  function materialize(loop2, fireId, stateDocs = []) {
127052
127599
  const dir = loopFireDir(loop2.slug, fireId);
127053
- mkdirSync9(dir, { recursive: true, mode: 448 });
127600
+ mkdirSync10(dir, { recursive: true, mode: 448 });
127054
127601
  for (const doc of stateDocs) {
127055
- writeFileSync11(join19(dir, doc.filename), doc.body, { mode: 384 });
127602
+ writeFileSync11(join20(dir, doc.filename), doc.body, { mode: 384 });
127056
127603
  }
127057
127604
  const files = {
127058
127605
  "SKILL.md": loop2.markdownBody ?? "",
@@ -127067,10 +127614,10 @@ function materialize(loop2, fireId, stateDocs = []) {
127067
127614
  files["dashboard.manifest.json"] = loop2.dashboardManifest;
127068
127615
  }
127069
127616
  for (const [name, body] of Object.entries(files)) {
127070
- writeFileSync11(join19(dir, name), body, { mode: 384 });
127617
+ writeFileSync11(join20(dir, name), body, { mode: 384 });
127071
127618
  }
127072
127619
  writeFileSync11(
127073
- join19(dir, "STATUS.md"),
127620
+ join20(dir, "STATUS.md"),
127074
127621
  `# STATUS \u2014 ${loop2.slug}
127075
127622
 
127076
127623
  loop_id: ${loop2.id}
@@ -127086,11 +127633,11 @@ queue: not started
127086
127633
  function mirrorDefinitionFiles(slug, files) {
127087
127634
  try {
127088
127635
  const root2 = loopDir(slug);
127089
- mkdirSync9(root2, { recursive: true, mode: 448 });
127636
+ mkdirSync10(root2, { recursive: true, mode: 448 });
127090
127637
  for (const name of [...MIRRORED_DEFINITION_FILES, "dashboard.html", "dashboard.manifest.json"]) {
127091
127638
  const body = files[name];
127092
127639
  if (body === void 0) continue;
127093
- writeFileSync11(join19(root2, name), body, { mode: 384 });
127640
+ writeFileSync11(join20(root2, name), body, { mode: 384 });
127094
127641
  }
127095
127642
  } catch {
127096
127643
  }
@@ -127216,15 +127763,15 @@ function resolveMaxBudgetUsd(opts, env = process.env) {
127216
127763
  // src/loops/heartbeat.ts
127217
127764
  init_esm_shims();
127218
127765
  import * as realFs2 from "fs";
127219
- import { join as join20 } from "path";
127766
+ import { join as join21 } from "path";
127220
127767
  function startHeartbeat(loopDir2, info, deps = {}) {
127221
127768
  const fs = deps.fs ?? realFs2;
127222
127769
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
127223
127770
  const setI = deps.setInterval ?? globalThis.setInterval;
127224
127771
  const clearI = deps.clearInterval ?? globalThis.clearInterval;
127225
127772
  const intervalMs = deps.intervalMs ?? 6e4;
127226
- const stateDir = join20(loopDir2, ".state");
127227
- const path2 = join20(stateDir, "fire.running");
127773
+ const stateDir = join21(loopDir2, ".state");
127774
+ const path2 = join21(stateDir, "fire.running");
127228
127775
  try {
127229
127776
  fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
127230
127777
  fs.writeFileSync(
@@ -127264,10 +127811,10 @@ ${info.sessionId ? `session=${info.sessionId}
127264
127811
  init_esm_shims();
127265
127812
  import * as realFs3 from "fs";
127266
127813
  import { homedir as homedir13 } from "os";
127267
- import { join as join21 } from "path";
127814
+ import { join as join22 } from "path";
127268
127815
  function transcriptPathFor(cwd, sessionId2, home = homedir13()) {
127269
127816
  const slug = cwd.replace(/\//g, "-");
127270
- return join21(home, ".claude", "projects", slug, `${sessionId2}.jsonl`);
127817
+ return join22(home, ".claude", "projects", slug, `${sessionId2}.jsonl`);
127271
127818
  }
127272
127819
  function parseTranscriptStats(jsonl) {
127273
127820
  const tokens = {
@@ -127338,9 +127885,9 @@ function recordFireAccounting(args) {
127338
127885
  }
127339
127886
  }
127340
127887
  function appendFireLine(fs, loopDir2, rec) {
127341
- const stateDir = join21(loopDir2, ".state");
127888
+ const stateDir = join22(loopDir2, ".state");
127342
127889
  fs.mkdirSync(stateDir, { recursive: true, mode: 448 });
127343
- fs.appendFileSync(join21(stateDir, "fires.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 });
127890
+ fs.appendFileSync(join22(stateDir, "fires.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 });
127344
127891
  }
127345
127892
  function recordFailedLaunch(args) {
127346
127893
  const fs = args.deps?.fs ?? realFs3;
@@ -127369,8 +127916,8 @@ function recordFailedLaunch(args) {
127369
127916
  init_esm_shims();
127370
127917
  import { spawn as spawn2 } from "child_process";
127371
127918
  import { randomUUID as randomUUID5 } from "crypto";
127372
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
127373
- import { dirname as dirname8, join as join23 } from "path";
127919
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
127920
+ import { dirname as dirname8, join as join24 } from "path";
127374
127921
 
127375
127922
  // src/loops/claude-binary.ts
127376
127923
  init_esm_shims();
@@ -127422,7 +127969,7 @@ function ensureClaudeBinary(deps = {}) {
127422
127969
  // src/loops/concurrency.ts
127423
127970
  init_esm_shims();
127424
127971
  import * as realFs4 from "fs";
127425
- import { join as join22 } from "path";
127972
+ import { join as join23 } from "path";
127426
127973
  var DEFAULT_MAX_CONCURRENT_FIRES = 6;
127427
127974
  var DEFAULT_HEARTBEAT_STALE_MS = 10 * 6e4;
127428
127975
  function pidIsAlive(pid) {
@@ -127446,14 +127993,14 @@ function liveFires(loopRoot, deps = {}) {
127446
127993
  const staleMs = deps.staleMs ?? DEFAULT_HEARTBEAT_STALE_MS;
127447
127994
  let entries;
127448
127995
  try {
127449
- entries = fs.readdirSync(join22(loopRoot, "fires"));
127996
+ entries = fs.readdirSync(join23(loopRoot, "fires"));
127450
127997
  } catch {
127451
127998
  return [];
127452
127999
  }
127453
128000
  const out = [];
127454
128001
  for (const entry of entries) {
127455
128002
  try {
127456
- const beat = join22(loopRoot, "fires", String(entry), ".state", "fire.running");
128003
+ const beat = join23(loopRoot, "fires", String(entry), ".state", "fire.running");
127457
128004
  const st = fs.statSync(beat);
127458
128005
  const heartbeatAgeMs = now() - Number(st.mtimeMs);
127459
128006
  if (!(heartbeatAgeMs <= staleMs)) continue;
@@ -127481,7 +128028,7 @@ var SHIPBACK_SIGNAL_TIMEOUT_MS = 8e3;
127481
128028
  function preserveRefusedConstraints(slug, fireId, body) {
127482
128029
  try {
127483
128030
  const path2 = refusedConstraintsPath(slug, fireId);
127484
- mkdirSync10(dirname8(path2), { recursive: true, mode: 448 });
128031
+ mkdirSync11(dirname8(path2), { recursive: true, mode: 448 });
127485
128032
  writeFileSync12(path2, body, { mode: 384 });
127486
128033
  return { path: path2 };
127487
128034
  } catch (err) {
@@ -127560,8 +128107,8 @@ async function runLoop(loopId, opts = {}) {
127560
128107
  console.log(
127561
128108
  `Loop ${loop2.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
127562
128109
  );
127563
- const statusPath = join23(dir, "STATUS.md");
127564
- const statusBefore = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
128110
+ const statusPath = join24(dir, "STATUS.md");
128111
+ const statusBefore = existsSync16(statusPath) ? readFileSync15(statusPath, "utf-8") : "";
127565
128112
  writeFileSync12(
127566
128113
  statusPath,
127567
128114
  statusBefore.replace(
@@ -127620,8 +128167,8 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127620
128167
  }
127621
128168
  async function shipConstraints() {
127622
128169
  try {
127623
- const constraintsPath = join23(dir, "CONSTRAINTS.md");
127624
- const materializedConstraints = existsSync15(constraintsPath) ? readFileSync14(constraintsPath, "utf-8") : void 0;
128170
+ const constraintsPath = join24(dir, "CONSTRAINTS.md");
128171
+ const materializedConstraints = existsSync16(constraintsPath) ? readFileSync15(constraintsPath, "utf-8") : void 0;
127625
128172
  if (materializedConstraints === void 0) return;
127626
128173
  const boot = loop2.constraintsMd ?? "";
127627
128174
  for (let attempt = 1; attempt <= 2; attempt++) {
@@ -127696,7 +128243,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127696
128243
  void Promise.race([shipBackConstraints().then(() => "shipped"), deadline]).then((outcome) => {
127697
128244
  if (outcome === "timeout") {
127698
128245
  console.error(
127699
- `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.`
128246
+ `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.`
127700
128247
  );
127701
128248
  }
127702
128249
  }).finally(() => process.exit(130));
@@ -127732,7 +128279,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
127732
128279
  startedAtEpochMs
127733
128280
  });
127734
128281
  }
127735
- const statusAfter = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
128282
+ const statusAfter = existsSync16(statusPath) ? readFileSync15(statusPath, "utf-8") : "";
127736
128283
  const clean2 = exitCode === 0 && /(vision\s*done|queue\s*drained|status:\s*done)/i.test(statusAfter) && !/(brake|crash|crashed|errored)/i.test(statusAfter);
127737
128284
  await shipBackConstraints();
127738
128285
  process.off("SIGINT", onSignal);
@@ -127770,9 +128317,9 @@ async function showLoop(loopId) {
127770
128317
  // src/loops/schedule.ts
127771
128318
  init_esm_shims();
127772
128319
  init_paths();
127773
- 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";
128320
+ 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";
127774
128321
  import { execFileSync as execFileSync4 } from "child_process";
127775
- import { join as join24, dirname as dirname9 } from "path";
128322
+ import { join as join25, dirname as dirname9 } from "path";
127776
128323
  import { homedir as homedir14 } from "os";
127777
128324
  var LOOP_KIND = {
127778
128325
  labelPrefix: "xyz.ametyst.loop.",
@@ -127783,7 +128330,7 @@ var LOOP_KIND = {
127783
128330
  return args;
127784
128331
  },
127785
128332
  stateDir(slug, cwd) {
127786
- return join24(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
128333
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
127787
128334
  }
127788
128335
  };
127789
128336
  var TASK_KIND = {
@@ -127795,7 +128342,7 @@ var TASK_KIND = {
127795
128342
  return args;
127796
128343
  },
127797
128344
  stateDir(slug, cwd) {
127798
- return join24(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
128345
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, "loops", safeSlug2(slug, this.noun), ".state");
127799
128346
  }
127800
128347
  };
127801
128348
  var COMPOUND_KIND = {
@@ -127807,7 +128354,7 @@ var COMPOUND_KIND = {
127807
128354
  return args;
127808
128355
  },
127809
128356
  stateDir(slug, cwd) {
127810
- return join24(cwd, `.ametyst${ENV_SUFFIX}`, "compounds", safeSlug2(slug, this.noun), ".state");
128357
+ return join25(cwd, `.ametyst${ENV_SUFFIX}`, "compounds", safeSlug2(slug, this.noun), ".state");
127811
128358
  }
127812
128359
  };
127813
128360
  function safeSlug2(slug, noun) {
@@ -127834,7 +128381,7 @@ function parseAt(at) {
127834
128381
  return { hour, minute };
127835
128382
  }
127836
128383
  function plistPath(kind, home, slug) {
127837
- return join24(home, "Library", "LaunchAgents", `${label(kind, slug)}.plist`);
128384
+ return join25(home, "Library", "LaunchAgents", `${label(kind, slug)}.plist`);
127838
128385
  }
127839
128386
  function launchctl(args) {
127840
128387
  try {
@@ -127850,7 +128397,7 @@ function isLoaded(lbl) {
127850
128397
  }
127851
128398
  function readInteractiveDefaultModel(home) {
127852
128399
  try {
127853
- const raw = readFileSync15(join24(home, ".claude", "settings.json"), "utf-8");
128400
+ const raw = readFileSync16(join25(home, ".claude", "settings.json"), "utf-8");
127854
128401
  const model = JSON.parse(String(raw)).model;
127855
128402
  return typeof model === "string" && model.trim() ? model.trim() : void 0;
127856
128403
  } catch {
@@ -127952,9 +128499,9 @@ ${progArgs}
127952
128499
  ${envEntries}
127953
128500
  </dict>
127954
128501
  <key>StandardOutPath</key>
127955
- <string>${escapeXml(join24(stateDir, LAUNCHD_OUT))}</string>
128502
+ <string>${escapeXml(join25(stateDir, LAUNCHD_OUT))}</string>
127956
128503
  <key>StandardErrorPath</key>
127957
- <string>${escapeXml(join24(stateDir, LAUNCHD_ERR))}</string>
128504
+ <string>${escapeXml(join25(stateDir, LAUNCHD_ERR))}</string>
127958
128505
  <key>AbandonProcessGroup</key>
127959
128506
  <true/>
127960
128507
  ${scheduleBlock}
@@ -128020,8 +128567,8 @@ function schedule(kind, slug, opts = {}) {
128020
128567
  const path2 = plistPath(kind, home, slug);
128021
128568
  const stateDir2 = kind.stateDir(slug, cwd);
128022
128569
  assertWritable(cwd, "working directory");
128023
- mkdirSync11(dirname9(path2), { recursive: true });
128024
- mkdirSync11(stateDir2, { recursive: true });
128570
+ mkdirSync12(dirname9(path2), { recursive: true });
128571
+ mkdirSync12(stateDir2, { recursive: true });
128025
128572
  assertWritable(stateDir2, "log directory");
128026
128573
  writeFileSync13(path2, plistXml(lbl, args, scheduleBlock, cwd, jobEnv, stateDir2), { mode: 384 });
128027
128574
  if (isLoaded(lbl)) {
@@ -128044,7 +128591,7 @@ launchctl said: ${loaded.output.trim()}` : "")
128044
128591
  }
128045
128592
  const stateDir = kind.stateDir(slug, cwd);
128046
128593
  assertWritable(cwd, "working directory");
128047
- mkdirSync11(stateDir, { recursive: true });
128594
+ mkdirSync12(stateDir, { recursive: true });
128048
128595
  assertWritable(stateDir, "log directory");
128049
128596
  const cronEnv = { PATH: envPath };
128050
128597
  if (model) cronEnv.ANTHROPIC_MODEL = model;
@@ -128077,9 +128624,9 @@ function list(kind, opts = {}) {
128077
128624
  const home = opts.home ?? homedir14();
128078
128625
  const prefix = kind.labelPrefix;
128079
128626
  if (platform === "darwin") {
128080
- const dir = join24(home, "Library", "LaunchAgents");
128081
- if (!existsSync16(dir)) return [];
128082
- return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
128627
+ const dir = join25(home, "Library", "LaunchAgents");
128628
+ if (!existsSync17(dir)) return [];
128629
+ return readdirSync6(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
128083
128630
  }
128084
128631
  return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length));
128085
128632
  }
@@ -128139,13 +128686,13 @@ function listEntries(kind, opts = {}) {
128139
128686
  const home = opts.home ?? homedir14();
128140
128687
  const prefix = kind.labelPrefix;
128141
128688
  if (platform === "darwin") {
128142
- const dir = join24(home, "Library", "LaunchAgents");
128143
- if (!existsSync16(dir)) return [];
128144
- return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
128689
+ const dir = join25(home, "Library", "LaunchAgents");
128690
+ if (!existsSync17(dir)) return [];
128691
+ return readdirSync6(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
128145
128692
  const slug = f.slice(prefix.length, -".plist".length);
128146
128693
  let envKeys = [];
128147
128694
  try {
128148
- envKeys = plistEnvKeys(String(readFileSync15(join24(dir, f), "utf-8")));
128695
+ envKeys = plistEnvKeys(String(readFileSync16(join25(dir, f), "utf-8")));
128149
128696
  } catch {
128150
128697
  envKeys = [];
128151
128698
  }